Last commit merged: f344831d58
This commit is contained in:
LuftVerbot 2023-10-29 15:12:39 +01:00
parent 9bb1a5da02
commit fd1c6437a6
59 changed files with 792 additions and 328 deletions

View file

@ -165,7 +165,8 @@ dependencies {
implementation(compose.material.icons)
implementation(compose.animation)
implementation(compose.animation.graphics)
implementation(compose.ui.tooling)
debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions)

View file

@ -95,10 +95,16 @@ fun Manga.hasCustomCover(coverCache: MangaCoverCache = Injekt.get()): Boolean {
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let { ComicInfo.Number(it.toString()) },
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
if ((it.rem(1) == 0.0F)) {
ComicInfo.Number(it.toInt().toString())
} else {
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
@ -108,6 +114,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
inker = null,
colorist = null,
letterer = null,

View file

@ -415,7 +415,7 @@ fun NsfwWarningDialog(
},
confirmButton = {
TextButton(onClick = onClickConfirm) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
onDismissRequest = onClickConfirm,

View file

@ -81,7 +81,7 @@ fun ChangeCategoryDialog(
)
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
}
},

View file

@ -97,7 +97,7 @@ fun CategoryRenameDialog(
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {
@ -147,7 +147,7 @@ fun CategoryDeleteDialog(
onDelete()
onDismissRequest()
},) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -23,7 +23,7 @@ internal fun Modifier.commonClickable(
) = composed {
val haptic = LocalHapticFeedback.current
this.combinedClickable(
Modifier.combinedClickable(
enabled = enabled,
onLongClick = {
onLongClick()

View file

@ -28,7 +28,7 @@ fun DeleteItemsDialog(
onConfirm()
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
title = {

View file

@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -94,7 +94,7 @@ fun HistoryDeleteAllDialog(
onDelete()
onDismissRequest()
},) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -56,7 +56,7 @@ fun DeleteLibraryEntryDialog(
)
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
title = {

View file

@ -83,7 +83,7 @@ class ClearAnimeDatabaseScreen : Screen() {
}
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -83,7 +83,7 @@ class ClearDatabaseScreen : Screen() {
}
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -411,7 +411,7 @@ object SettingsAdvancedScreen : SearchableSettings {
uriHandler.openUri("https://shizuku.rikka.app/download")
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -213,7 +213,7 @@ object SettingsBackupScreen : SearchableSettings {
onConfirm(flag)
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)
@ -266,7 +266,7 @@ object SettingsBackupScreen : SearchableSettings {
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -516,7 +516,7 @@ object SettingsLibraryScreen : SearchableSettings {
},
confirmButton = {
TextButton(onClick = { onValueChanged(leadValue, followValue) }) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -379,7 +379,7 @@ object SettingsPlayerScreen : SearchableSettings {
},
confirmButton = {
TextButton(onClick = { onValueChanged(newLength) }) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -194,11 +194,6 @@ object SettingsReaderScreen : SearchableSettings {
6 to stringResource(R.string.scale_type_smart_fit),
),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.landscapeZoom(),
title = stringResource(R.string.pref_landscape_zoom),
enabled = imageScaleType == 1,
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(),
title = stringResource(R.string.pref_zoom_start),
@ -214,6 +209,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPreferences.cropBorders(),
title = stringResource(R.string.pref_crop_borders),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.landscapeZoom(),
title = stringResource(R.string.pref_landscape_zoom),
enabled = imageScaleType == 1,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.navigateToPan(),
title = stringResource(R.string.pref_navigate_pan),

View file

@ -113,7 +113,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
tween(200)
},
)
then(Modifier.background(color = highlight))
Modifier.background(color = highlight)
}
internal val TrailingWidgetBuffer = 16.dp

View file

@ -84,7 +84,7 @@ fun EditTextPreferenceWidget(
}
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -96,7 +96,7 @@ fun MultiSelectListPreferenceWidget(
isDialogShown = false
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -137,7 +137,7 @@ fun <T> TriStateListDialog(
onValueChanged(included, excluded)
},
) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -127,7 +127,7 @@ fun TrackScoreSelector(
content = {
WheelTextPicker(
modifier = Modifier.align(Alignment.Center),
startIndex = selections.indexOf(selection).coerceAtLeast(0),
startIndex = selections.indexOf(selection).takeIf { it > 0 } ?: (selections.size / 2),
items = selections,
onSelectionChanged = { onSelectionChange(selections[it]) },
)
@ -177,7 +177,7 @@ fun TrackDateSelector(
Text(text = stringResource(android.R.string.cancel))
}
TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
}
}
@ -215,7 +215,7 @@ fun BaseSelector(
Text(text = stringResource(android.R.string.cancel))
}
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
}
},

View file

@ -24,7 +24,7 @@ fun UpdatesDeleteConfirmationDialog(
onConfirm()
onDismissRequest()
},) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
dismissButton = {

View file

@ -28,25 +28,11 @@ import java.io.IOException
*/
class ChapterCache(private val context: Context) {
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
/** Application cache version. */
const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
}
private val json: Json by injectLazy()
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
File(context.cacheDir, "chapter_disk_cache"),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE,
@ -55,8 +41,7 @@ class ChapterCache(private val context: Context) {
/**
* Returns directory of cache.
*/
private val cacheDir: File
get() = diskCache.directory
private val cacheDir: File = diskCache.directory
/**
* Returns real size of directory.
@ -210,3 +195,12 @@ class ChapterCache(private val context: Context) {
return "${chapter.mangaId}${chapter.url}"
}
}
/** Application cache version. */
private const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
private const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024

View file

@ -27,25 +27,11 @@ import java.io.IOException
*/
class EpisodeCache(private val context: Context) {
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "episode_disk_cache"
/** Application cache version. */
const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
const val PARAMETER_CACHE_SIZE = 1000L * 1024 * 1024
}
private val json: Json by injectLazy()
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
File(context.cacheDir, "episode_disk_cache"),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE,
@ -54,8 +40,7 @@ class EpisodeCache(private val context: Context) {
/**
* Returns directory of cache.
*/
val cacheDir: File
get() = diskCache.directory
val cacheDir: File = diskCache.directory
/**
* Returns real size of directory.
@ -193,3 +178,12 @@ class EpisodeCache(private val context: Context) {
return "${episode.animeId}${episode.url}"
}
}
/** Application cache version. */
private const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
private const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024

View file

@ -53,6 +53,7 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
@ -82,6 +83,7 @@ class MangaDownloader(
private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val xml: XML = Injekt.get(),
private val getCategories: GetMangaCategories = Injekt.get(),
// SY -->
private val sourcePreferences: SourcePreferences = Injekt.get(),
// SY <--
@ -634,14 +636,15 @@ class MangaDownloader(
/**
* Creates a ComicInfo.xml file inside the given directory.
*/
private fun createComicInfoFile(
private suspend fun createComicInfoFile(
dir: UniFile,
manga: Manga,
chapter: Chapter,
source: HttpSource,
) {
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
val comicInfo = getComicInfo(manga, chapter, chapterUrl)
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
// Remove the old file
dir.findFile(COMIC_INFO_FILE)?.delete()
dir.createFile(COMIC_INFO_FILE).openOutputStream().use {

View file

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.track
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
/**
* For track services api that support deleting a manga entry for a user's list
*/
interface DeletableAnimeTrackService {
suspend fun delete(track: AnimeTrack): AnimeTrack
}

View file

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.track
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
/**
* For track services api that support deleting a manga entry for a user's list
*/
interface DeletableMangaTrackService {
suspend fun delete(track: MangaTrack): MangaTrack
}

View file

@ -6,6 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableAnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -17,7 +19,7 @@ import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
class Anilist(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService {
class Anilist(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService, DeletableMangaTrackService, DeletableAnimeTrackService {
companion object {
const val READING = 1
@ -238,6 +240,24 @@ class Anilist(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService
return api.updateLibAnime(track)
}
override suspend fun delete(track: MangaTrack): MangaTrack {
if (track.library_id == null || track.library_id!! == 0L) {
val libManga = api.findLibManga(track, getUsername().toInt()) ?: return track
track.library_id = libManga.library_id
}
return api.deleteLibManga(track)
}
override suspend fun delete(track: AnimeTrack): AnimeTrack {
if (track.library_id == null || track.library_id!! == 0L) {
val libManga = api.findLibAnime(track, getUsername().toInt()) ?: return track
track.library_id = libManga.library_id
}
return api.deleteLibAnime(track)
}
override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) {

View file

@ -112,6 +112,28 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
suspend fun deleteLibManga(track: MangaTrack): MangaTrack {
return withIOContext {
val query = """
|mutation DeleteManga(${'$'}listId: Int) {
|DeleteMediaListEntry(id: ${'$'}listId) {
|deleted
|}
|}
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("listId", track.library_id)
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
}
suspend fun addLibAnime(track: AnimeTrack): AnimeTrack {
return withIOContext {
val query = """
@ -184,6 +206,28 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
suspend fun deleteLibAnime(track: AnimeTrack): AnimeTrack {
return withIOContext {
val query = """
|mutation DeleteAnime(${'$'}listId: Int) {
|DeleteMediaListEntry(id: ${'$'}listId) {
|deleted
|}
|}
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("listId", track.library_id)
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
}
suspend fun search(search: String): List<MangaTrackSearch> {
return withIOContext {
val query = """

View file

@ -6,6 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableAnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -16,7 +18,7 @@ import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService {
class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService, DeletableMangaTrackService, DeletableAnimeTrackService {
companion object {
const val READING = 1
@ -136,6 +138,14 @@ class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService {
return api.updateLibAnime(track)
}
override suspend fun delete(track: MangaTrack): MangaTrack {
return api.removeLibManga(track)
}
override suspend fun delete(track: AnimeTrack): AnimeTrack {
return api.removeLibAnime(track)
}
override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) {

View file

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
@ -213,6 +214,38 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
}
suspend fun removeLibManga(track: MangaTrack): MangaTrack {
return withIOContext {
authClient.newCall(
DELETE(
"${baseUrl}library-entries/${track.media_id}",
headers = headersOf(
"Content-Type",
"application/vnd.api+json",
),
),
)
.awaitSuccess()
track
}
}
suspend fun removeLibAnime(track: AnimeTrack): AnimeTrack {
return withIOContext {
authClient.newCall(
DELETE(
"${baseUrl}library-entries/${track.media_id}",
headers = headersOf(
"Content-Type",
"application/vnd.api+json",
),
),
)
.awaitSuccess()
track
}
}
suspend fun search(query: String): List<MangaTrackSearch> {
return withIOContext {
with(json) {

View file

@ -4,13 +4,14 @@ import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
class MangaUpdates(id: Long) : TrackService(id), MangaTrackService {
class MangaUpdates(id: Long) : TrackService(id), MangaTrackService, DeletableMangaTrackService {
companion object {
const val READING_LIST = 0
@ -67,6 +68,11 @@ class MangaUpdates(id: Long) : TrackService(id), MangaTrackService {
return track
}
override suspend fun delete(track: MangaTrack): MangaTrack {
api.deleteSeriesFromList(track)
return track
}
override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
return try {
val (series, rating) = api.getSeriesListItem(track)

View file

@ -106,6 +106,19 @@ class MangaUpdatesApi(
updateSeriesRating(track)
}
suspend fun deleteSeriesFromList(track: MangaTrack) {
val body = buildJsonArray {
add(track.media_id)
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series/delete",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
}
private suspend fun getSeriesRating(track: MangaTrack): Rating? {
return try {
with(json) {

View file

@ -6,6 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableAnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -15,7 +17,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
class MyAnimeList(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService {
class MyAnimeList(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService, DeletableMangaTrackService, DeletableAnimeTrackService {
companion object {
const val READING = 1
@ -140,6 +142,14 @@ class MyAnimeList(id: Long) : TrackService(id), MangaTrackService, AnimeTrackSer
return api.updateItem(track)
}
override suspend fun delete(track: MangaTrack): MangaTrack {
return api.deleteMangaItem(track)
}
override suspend fun delete(track: AnimeTrack): AnimeTrack {
return api.deleteAnimeItem(track)
}
override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findListItem(track)
return if (remoteTrack != null) {

View file

@ -249,6 +249,34 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
}
suspend fun deleteMangaItem(track: MangaTrack): MangaTrack {
return withIOContext {
val request = Request.Builder()
.url(mangaUrl(track.media_id).toString())
.delete()
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
track
}
}
}
suspend fun deleteAnimeItem(track: AnimeTrack): AnimeTrack {
return withIOContext {
val request = Request.Builder()
.url(animeUrl(track.media_id).toString())
.delete()
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
track
}
}
}
suspend fun findListItem(track: MangaTrack): MangaTrack? {
return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon()

View file

@ -6,6 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableAnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -15,7 +17,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService {
class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService, DeletableMangaTrackService, DeletableAnimeTrackService {
companion object {
const val READING = 1
@ -87,6 +89,14 @@ class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackServi
return api.updateLibAnime(track, getUsername())
}
override suspend fun delete(track: MangaTrack): MangaTrack {
return api.deleteLibManga(track)
}
override suspend fun delete(track: AnimeTrack): AnimeTrack {
return api.deleteLibAnime(track)
}
override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUsername())
return if (remoteTrack != null) {
@ -137,6 +147,7 @@ class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackServi
override suspend fun refresh(track: MangaTrack): MangaTrack {
api.findLibManga(track, getUsername())?.let { remoteTrack ->
track.library_id = remoteTrack.library_id
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
}
@ -145,6 +156,7 @@ class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackServi
override suspend fun refresh(track: AnimeTrack): AnimeTrack {
api.findLibAnime(track, getUsername())?.let { remoteTrack ->
track.library_id = remoteTrack.library_id
track.copyPersonalFrom(remoteTrack)
track.total_episodes = remoteTrack.total_episodes
}

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
@ -37,6 +38,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
suspend fun addLibManga(track: MangaTrack, user_id: String): MangaTrack {
return withIOContext {
with(json) {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
@ -52,6 +54,24 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
"$apiUrl/v2/user_rates",
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let {
track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
}
track
}
}
}
suspend fun updateLibManga(track: MangaTrack, user_id: String): MangaTrack = addLibManga(track, user_id)
suspend fun deleteLibManga(track: MangaTrack): MangaTrack {
return withIOContext {
authClient.newCall(
DELETE(
"$apiUrl/v2/user_rates/${track.library_id}",
),
).awaitSuccess()
track
}
@ -59,11 +79,12 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
suspend fun addLibAnime(track: AnimeTrack, user_id: String): AnimeTrack {
return withIOContext {
with(json) {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("target_id", track.media_id)
put("target_type", "Manga")
put("target_type", "Anime")
put("chapters", track.last_episode_seen.toInt())
put("score", track.score.toInt())
put("status", track.toShikimoriStatus())
@ -75,14 +96,28 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let {
track.library_id = it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request
}
track
}
}
suspend fun updateLibManga(track: MangaTrack, user_id: String): MangaTrack = addLibManga(track, user_id)
}
suspend fun updateLibAnime(track: AnimeTrack, user_id: String): AnimeTrack = addLibAnime(track, user_id)
suspend fun deleteLibAnime(track: AnimeTrack): AnimeTrack {
return withIOContext {
authClient.newCall(
DELETE(
"$apiUrl/v2/user_rates/${track.library_id}",
),
).awaitSuccess()
track
}
}
suspend fun search(search: String): List<MangaTrackSearch> {
return withIOContext {
val url = "$apiUrl/mangas".toUri().buildUpon()
@ -156,6 +191,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
title = mangas["name"]!!.jsonPrimitive.content
media_id = obj["id"]!!.jsonPrimitive.long
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
library_id = obj["id"]!!.jsonPrimitive.long
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
score = (obj["score"]!!.jsonPrimitive.int).toFloat()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
@ -168,6 +204,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
title = animes["name"]!!.jsonPrimitive.content
media_id = obj["id"]!!.jsonPrimitive.long
total_episodes = animes["episodes"]!!.jsonPrimitive.int
library_id = obj["id"]!!.jsonPrimitive.long
last_episode_seen = obj["episodes"]!!.jsonPrimitive.float
score = (obj["score"]!!.jsonPrimitive.int).toFloat()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.entries.anime.track
import android.app.Application
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -49,6 +51,7 @@ import eu.kanade.presentation.track.anime.AnimeTrackServiceSearch
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.DeletableAnimeTrackService
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
@ -158,7 +161,16 @@ data class AnimeTrackInfoDialogHomeScreen(
}
},
onOpenInBrowser = { openTrackerInBrowser(context, it) },
) { sm.unregisterTracking(it.service.id) }
onRemoved = {
navigator.push(
TrackAnimeServiceRemoveScreen(
animeId = animeId,
track = it.track!!,
serviceId = it.service.id,
),
)
},
)
}
/**
@ -175,7 +187,6 @@ data class AnimeTrackInfoDialogHomeScreen(
private val animeId: Long,
private val sourceId: Long,
private val getTracks: GetAnimeTracks = Injekt.get(),
private val deleteTrack: DeleteAnimeTrack = Injekt.get(),
) : StateScreenModel<Model.State>(State()) {
init {
@ -205,10 +216,6 @@ data class AnimeTrackInfoDialogHomeScreen(
}
}
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(animeId, serviceId) }
}
private suspend fun refreshTrackers() {
val insertAnimeTrack = Injekt.get<InsertAnimeTrack>()
val getAnimeWithEpisodes = Injekt.get<GetAnimeWithEpisodes>()
@ -730,3 +737,100 @@ data class TrackServiceSearchScreen(
)
}
}
private data class TrackAnimeServiceRemoveScreen(
private val animeId: Long,
private val track: AnimeTrack,
private val serviceId: Long,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
Model(
animeId = animeId,
track = track,
service = Injekt.get<TrackManager>().getService(serviceId)!!,
)
}
val serviceName = stringResource(sm.getServiceNameRes())
var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
},
title = {
Text(
text = stringResource(R.string.track_delete_title, serviceName),
textAlign = TextAlign.Center,
)
},
text = {
Column {
Text(
text = stringResource(R.string.track_delete_text, serviceName),
)
if (sm.isServiceDeletable()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = removeRemoteTrack, onCheckedChange = { removeRemoteTrack = it })
Text(text = stringResource(R.string.track_delete_remote_text, serviceName))
}
}
}
},
buttons = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
MaterialTheme.padding.small,
Alignment.End,
),
) {
TextButton(onClick = navigator::pop) {
Text(text = stringResource(R.string.action_cancel))
}
FilledTonalButton(
onClick = {
sm.unregisterTracking(serviceId)
if (removeRemoteTrack) sm.deleteAnimeFromService()
navigator.pop()
},
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
) {
Text(text = stringResource(R.string.action_ok))
}
}
},
)
}
private class Model(
private val animeId: Long,
private val track: AnimeTrack,
private val service: TrackService,
private val deleteTrack: DeleteAnimeTrack = Injekt.get(),
) : ScreenModel {
fun getServiceNameRes() = service.nameRes()
fun isServiceDeletable() = service is DeletableAnimeTrackService
fun deleteAnimeFromService() {
coroutineScope.launchNonCancellable {
(service as DeletableAnimeTrackService).delete(track.toDbTrack())
}
}
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(animeId, serviceId) }
}
}
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.entries.manga.track
import android.app.Application
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -48,6 +50,7 @@ import eu.kanade.presentation.track.manga.MangaTrackInfoDialogHome
import eu.kanade.presentation.track.manga.MangaTrackServiceSearch
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.DeletableMangaTrackService
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
@ -158,7 +161,16 @@ data class MangaTrackInfoDialogHomeScreen(
}
},
onOpenInBrowser = { openTrackerInBrowser(context, it) },
) { sm.unregisterTracking(it.service.id) }
onRemoved = {
navigator.push(
TrackMangaServiceRemoveScreen(
mangaId = mangaId,
track = it.track!!,
serviceId = it.service.id,
),
)
},
)
}
/**
@ -175,7 +187,6 @@ data class MangaTrackInfoDialogHomeScreen(
private val mangaId: Long,
private val sourceId: Long,
private val getTracks: GetMangaTracks = Injekt.get(),
private val deleteTrack: DeleteMangaTrack = Injekt.get(),
) : StateScreenModel<Model.State>(State()) {
init {
@ -205,10 +216,6 @@ data class MangaTrackInfoDialogHomeScreen(
}
}
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(mangaId, serviceId) }
}
private suspend fun refreshTrackers() {
val insertTrack = Injekt.get<InsertMangaTrack>()
val getMangaWithChapters = Injekt.get<GetMangaWithChapters>()
@ -729,3 +736,100 @@ data class TrackServiceSearchScreen(
)
}
}
private data class TrackMangaServiceRemoveScreen(
private val mangaId: Long,
private val track: MangaTrack,
private val serviceId: Long,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val sm = rememberScreenModel {
Model(
mangaId = mangaId,
track = track,
service = Injekt.get<TrackManager>().getService(serviceId)!!,
)
}
val serviceName = stringResource(sm.getServiceNameRes())
var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
},
title = {
Text(
text = stringResource(R.string.track_delete_title, serviceName),
textAlign = TextAlign.Center,
)
},
text = {
Column {
Text(
text = stringResource(R.string.track_delete_text, serviceName),
)
if (sm.isServiceDeletable()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = removeRemoteTrack, onCheckedChange = { removeRemoteTrack = it })
Text(text = stringResource(R.string.track_delete_remote_text, serviceName))
}
}
}
},
buttons = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
MaterialTheme.padding.small,
Alignment.End,
),
) {
TextButton(onClick = navigator::pop) {
Text(text = stringResource(R.string.action_cancel))
}
FilledTonalButton(
onClick = {
sm.unregisterTracking(serviceId)
if (removeRemoteTrack) sm.deleteMangaFromService()
navigator.pop()
},
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
) {
Text(text = stringResource(R.string.action_ok))
}
}
},
)
}
private class Model(
private val mangaId: Long,
private val track: MangaTrack,
private val service: TrackService,
private val deleteTrack: DeleteMangaTrack = Injekt.get(),
) : ScreenModel {
fun getServiceNameRes() = service.nameRes()
fun isServiceDeletable() = service is DeletableMangaTrackService
fun deleteMangaFromService() {
coroutineScope.launchNonCancellable {
(service as DeletableMangaTrackService).delete(track.toDbTrack())
}
}
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(mangaId, serviceId) }
}
}
}

View file

@ -273,7 +273,7 @@ class MainActivity : BaseActivity() {
},
confirmButton = {
TextButton(onClick = { showChangelog = false }) {
Text(text = stringResource(android.R.string.ok))
Text(text = stringResource(R.string.action_ok))
}
},
)

View file

@ -20,7 +20,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.TextButton
// TODO: (Merge_Change) stringResource "android.R.string.ok" to be replaced with
// TODO: (Merge_Change) stringResource "R.string.action_ok" to be replaced with
// "R.string.action_ok"
@Composable
@ -71,7 +71,7 @@ fun PlayerDialog(
}
TextButton(onClick = onConfirm) {
Text(stringResource(android.R.string.ok))
Text(stringResource(R.string.action_ok))
}
}
}

View file

@ -411,6 +411,20 @@ class ReaderActivity : BaseActivity() {
)
}
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
when (state.dialog) {
is ReaderViewModel.Dialog.Page -> ReaderPageDialog(
onDismissRequest = viewModel::closeDialog,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
null -> {}
}
}
// Init listeners on bottom menu
binding.readerNav.setComposeContent {
val state by viewModel.state.collectAsState()
@ -790,7 +804,7 @@ class ReaderActivity : BaseActivity() {
* actions to perform is shown.
*/
fun onPageLongTap(page: ReaderPage) {
ReaderPageSheet(this, page).show()
viewModel.openPageDialog(page)
}
/**
@ -827,14 +841,6 @@ class ReaderActivity : BaseActivity() {
}
}
/**
* Called from the page sheet. It delegates the call to the presenter to do some IO, which
* will call [onShareImageResult] with the path the image was saved on when it's ready.
*/
fun shareImage(page: ReaderPage) {
viewModel.shareImage(page)
}
/**
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
@ -850,14 +856,6 @@ class ReaderActivity : BaseActivity() {
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
/**
* Called from the page sheet. It delegates saving the image of the given [page] on external
* storage to the presenter.
*/
fun saveImage(page: ReaderPage) {
viewModel.saveImage(page)
}
/**
* Called from the presenter when a page is saved or fails. It shows a message or logs the
* event depending on the [result].
@ -873,14 +871,6 @@ class ReaderActivity : BaseActivity() {
}
}
/**
* Called from the page sheet. It delegates setting the image of the given [page] as the
* cover to the presenter.
*/
fun setAsCover(page: ReaderPage) {
viewModel.setAsCover(page)
}
/**
* Called from the presenter when a page is set as cover or fails. It shows a different message
* depending on the [result].

View file

@ -0,0 +1,102 @@
package eu.kanade.tachiyomi.ui.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding
@Composable
fun ReaderPageDialog(
onDismissRequest: () -> Unit,
onSetAsCover: () -> Unit,
onShare: () -> Unit,
onSave: () -> Unit,
) {
var showSetCoverDialog by remember { mutableStateOf(false) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.set_as_cover),
icon = Icons.Outlined.Photo,
onClick = { showSetCoverDialog = true },
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_share),
icon = Icons.Outlined.Share,
onClick = {
onShare()
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_save),
icon = Icons.Outlined.Save,
onClick = {
onSave()
onDismissRequest()
},
)
}
}
if (showSetCoverDialog) {
SetCoverDialog(
onConfirm = {
onSetAsCover()
showSetCoverDialog = false
},
onDismiss = { showSetCoverDialog = false },
)
}
}
@Composable
private fun SetCoverDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
text = {
Text(stringResource(R.string.confirm_set_image_as_cover))
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
},
onDismissRequest = onDismiss,
)
}

View file

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.ui.reader
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderPageSheetBinding
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
/**
* Sheet to show when a page is long clicked.
*/
class ReaderPageSheet(
private val activity: ReaderActivity,
private val page: ReaderPage,
) : BaseBottomSheetDialog(activity) {
private lateinit var binding: ReaderPageSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = ReaderPageSheetBinding.inflate(activity.layoutInflater, null, false)
binding.setAsCover.setOnClickListener { setAsCover() }
binding.share.setOnClickListener { share() }
binding.save.setOnClickListener { save() }
return binding.root
}
/**
* Sets the image of this page as the cover of the manga.
*/
private fun setAsCover() {
if (page.status != Page.State.READY) return
MaterialAlertDialogBuilder(activity)
.setMessage(R.string.confirm_set_image_as_cover)
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.setAsCover(page)
}
.setNegativeButton(R.string.action_cancel, null)
.show()
}
/**
* Shares the image of this page with external apps.
*/
private fun share() {
activity.shareImage(page)
dismiss()
}
/**
* Saves the image of this page on external storage.
*/
private fun save() {
activity.saveImage(page)
dismiss()
}
}

View file

@ -718,12 +718,21 @@ class ReaderViewModel(
) + filenameSuffix
}
fun openPageDialog(page: ReaderPage) {
mutableState.update { it.copy(dialog = Dialog.Page(page)) }
}
fun closeDialog() {
mutableState.update { it.copy(dialog = null) }
}
/**
* Saves the image of this [page] on the pictures directory and notifies the UI of the result.
* Saves the image of this the selected page on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
*/
fun saveImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun saveImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -757,14 +766,15 @@ class ReaderViewModel(
}
/**
* Shares the image of this [page] and notifies the UI with the path of the file to share.
* Shares the image of this the selected page and notifies the UI with the path of the file to share.
* The image must be first copied to the internal partition because there are many possible
* formats it can come from, like a zipped chapter, in which case it's not possible to directly
* get a path to the file and it has to be decompressed somewhere first. Only the last shared
* image will be kept so it won't be taking lots of internal disk space.
*/
fun shareImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun shareImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -790,10 +800,11 @@ class ReaderViewModel(
}
/**
* Sets the image of this [page] as cover and notifies the UI of the result.
* Sets the image of this the selected page as cover and notifies the UI of the result.
*/
fun setAsCover(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun setAsCover() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val stream = page.stream ?: return
@ -906,11 +917,16 @@ class ReaderViewModel(
* Viewer used to display the pages (pager, webtoon, ...).
*/
val viewer: Viewer? = null,
val dialog: Dialog? = null,
) {
val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1
}
sealed class Dialog {
data class Page(val page: ReaderPage) : Dialog()
}
sealed class Event {
object ReloadViewerChapters : Event()
data class SetOrientation(val orientation: Int) : Event()

View file

@ -138,4 +138,9 @@
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/set_as_cover"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/set_as_cover"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_photo_24dp"
app:drawableTint="?attr/colorOnBackground" />
<TextView
android:id="@+id/share"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/action_share"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_share_24dp"
app:drawableTint="?attr/colorOnBackground" />
<TextView
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/action_save"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_save_24dp"
app:drawableTint="?attr/colorOnBackground" />
</LinearLayout>

View file

@ -38,15 +38,6 @@
android:entries="@array/image_scale_type"
app:title="@string/pref_image_scale_type" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/landscape_zoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_landscape_zoom"
android:textColor="?android:attr/textColorSecondary" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/zoom_start"
android:layout_width="match_parent"
@ -63,6 +54,15 @@
android:text="@string/pref_crop_borders"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/landscape_zoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_landscape_zoom"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/navigate_pan"
android:layout_width="match_parent"

View file

@ -16,8 +16,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
listOfNotNull(
comicInfo.genre?.value,
comicInfo.tags?.value,
comicInfo.categories?.value,
)
.flatMap { it.split(", ") }
.distinct()
.joinToString(", ") { it.trim() }
.takeIf { it.isNotEmpty() }
@ -57,6 +57,7 @@ data class ComicInfo(
val tags: Tags?,
val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?,
) {
@Suppress("UNUSED")
@XmlElement(false)
@ -128,6 +129,10 @@ data class ComicInfo(
@Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
}
enum class ComicInfoPublishingStatus(

View file

@ -17,6 +17,6 @@ class NetworkPreferences(
}
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/111.0")
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0")
}
}

View file

@ -10,6 +10,7 @@ foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" }
animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
ui-util = { module = "androidx.compose.ui:ui-util" }
material3-core = { module = "androidx.compose.material3:material3" }

View file

@ -121,6 +121,7 @@
<string name="action_pin">Pin</string>
<string name="action_unpin">Unpin</string>
<string name="action_cancel">Cancel</string>
<string name="action_ok">OK</string>
<string name="action_cancel_all">Cancel all</string>
<string name="cancel_all_for_series">Cancel all for this series</string>
<string name="action_sort">Sort</string>
@ -404,7 +405,7 @@
<string name="scale_type_original_size">Original size</string>
<string name="scale_type_smart_fit">Smart fit</string>
<string name="pref_navigate_pan">Pan wide images</string>
<string name="pref_landscape_zoom">Zoom landscape image</string>
<string name="pref_landscape_zoom">Automatically zoom into wide images</string>
<string name="pref_zoom_start">Zoom start position</string>
<string name="zoom_start_automatic">Automatic</string>
<string name="zoom_start_left">Left</string>
@ -739,6 +740,9 @@
<string name="track_remove_date_conf_title">Remove date?</string>
<string name="track_remove_start_date_conf_text">This will remove your previously selected start date from %s</string>
<string name="track_remove_finish_date_conf_text">This will remove your previously selected finish date from %s</string>
<string name="track_delete_title">Remove %s tracking?</string>
<string name="track_delete_text">This will remove the tracking locally.</string>
<string name="track_delete_remote_text">Also remove from %s</string>
<!-- Category activity -->
<string name="error_category_exists">A category with this name already exists!</string>

View file

@ -30,7 +30,8 @@ dependencies {
implementation(compose.material.icons)
implementation(compose.animation)
implementation(compose.animation.graphics)
implementation(compose.ui.tooling)
debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
}

View file

@ -0,0 +1,40 @@
package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -30,7 +30,9 @@ import tachiyomi.presentation.core.components.Pill
private fun Modifier.tabIndicatorOffset(
currentTabPosition: TabPosition,
currentPageOffsetFraction: Float,
) = composed {
) = fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.composed {
val currentTabWidth by animateDpAsState(
targetValue = currentTabPosition.width,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
@ -39,8 +41,7 @@ private fun Modifier.tabIndicatorOffset(
targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
)
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
Modifier
.offset { IntOffset(x = offset.roundToPx(), y = 0) }
.width(currentTabWidth)
}

View file

@ -4,17 +4,13 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -24,6 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.random.Random
@ -96,31 +93,6 @@ fun EmptyScreen(
}
}
@Composable
private fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = icon,
contentDescription = null,
)
Spacer(Modifier.height(4.dp))
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}
private val ERROR_FACES = listOf(
"(・o・;)",
"Σ(ಠ_ಠ)",

View file

@ -25,14 +25,14 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalFocusManager
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
if (isSelected) {
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = if (isSelected) {
composed {
val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f
background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
Modifier.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
}
} else {
this
}
}
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha)
@ -40,7 +40,7 @@ fun Modifier.clickableNoIndication(
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit,
): Modifier = composed {
this.combinedClickable(
Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onLongClick = onLongClick,