mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-27 16:26:29 +03:00
parent
9bb1a5da02
commit
fd1c6437a6
59 changed files with 792 additions and 328 deletions
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -81,7 +81,7 @@ fun ChangeCategoryDialog(
|
|||
)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -23,7 +23,7 @@ internal fun Modifier.commonClickable(
|
|||
) = composed {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
this.combinedClickable(
|
||||
Modifier.combinedClickable(
|
||||
enabled = enabled,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
|
|
|
@ -28,7 +28,7 @@ fun DeleteItemsDialog(
|
|||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
|
|
|
@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
|
|||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
|
|||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -94,7 +94,7 @@ fun HistoryDeleteAllDialog(
|
|||
onDelete()
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -56,7 +56,7 @@ fun DeleteLibraryEntryDialog(
|
|||
)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
|
|
|
@ -83,7 +83,7 @@ class ClearAnimeDatabaseScreen : Screen() {
|
|||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -83,7 +83,7 @@ class ClearDatabaseScreen : Screen() {
|
|||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -84,7 +84,7 @@ fun EditTextPreferenceWidget(
|
|||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -96,7 +96,7 @@ fun MultiSelectListPreferenceWidget(
|
|||
isDialogShown = false
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -137,7 +137,7 @@ fun <T> TriStateListDialog(
|
|||
onValueChanged(included, excluded)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ fun UpdatesDeleteConfirmationDialog(
|
|||
onConfirm()
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
|
|
|
@ -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. */
|
||||
/** 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
|
||||
|
|
|
@ -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. */
|
||||
/** 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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 = """
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,20 +38,39 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||
|
||||
suspend fun addLibManga(track: MangaTrack, user_id: String): MangaTrack {
|
||||
return withIOContext {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_chapter_read.toInt())
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
with(json) {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_chapter_read.toInt())
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
}
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$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(
|
||||
POST(
|
||||
"$apiUrl/v2/user_rates",
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
DELETE(
|
||||
"$apiUrl/v2/user_rates/${track.library_id}",
|
||||
),
|
||||
).awaitSuccess()
|
||||
track
|
||||
|
@ -59,30 +79,45 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||
|
||||
suspend fun addLibAnime(track: AnimeTrack, user_id: String): AnimeTrack {
|
||||
return withIOContext {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_episode_seen.toInt())
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
with(json) {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Anime")
|
||||
put("chapters", track.last_episode_seen.toInt())
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
}
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$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 updateLibAnime(track: AnimeTrack, user_id: String): AnimeTrack = addLibAnime(track, user_id)
|
||||
|
||||
suspend fun deleteLibAnime(track: AnimeTrack): AnimeTrack {
|
||||
return withIOContext {
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$apiUrl/v2/user_rates",
|
||||
body = payload.toString().toRequestBody(jsonMime),
|
||||
DELETE(
|
||||
"$apiUrl/v2/user_rates/${track.library_id}",
|
||||
),
|
||||
).awaitSuccess()
|
||||
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 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)
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,20 +30,21 @@ import tachiyomi.presentation.core.components.Pill
|
|||
private fun Modifier.tabIndicatorOffset(
|
||||
currentTabPosition: TabPosition,
|
||||
currentPageOffsetFraction: Float,
|
||||
) = composed {
|
||||
val currentTabWidth by animateDpAsState(
|
||||
targetValue = currentTabPosition.width,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
)
|
||||
val offset by animateDpAsState(
|
||||
targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
)
|
||||
fillMaxWidth()
|
||||
.wrapContentSize(Alignment.BottomStart)
|
||||
.offset { IntOffset(x = offset.roundToPx(), y = 0) }
|
||||
.width(currentTabWidth)
|
||||
}
|
||||
) = fillMaxWidth()
|
||||
.wrapContentSize(Alignment.BottomStart)
|
||||
.composed {
|
||||
val currentTabWidth by animateDpAsState(
|
||||
targetValue = currentTabPosition.width,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
)
|
||||
val offset by animateDpAsState(
|
||||
targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
)
|
||||
Modifier
|
||||
.offset { IntOffset(x = offset.roundToPx(), y = 0) }
|
||||
.width(currentTabWidth)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabIndicator(
|
||||
|
|
|
@ -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・;)",
|
||||
"Σ(ಠ_ಠ)",
|
||||
|
|
|
@ -25,13 +25,13 @@ 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))
|
||||
} else {
|
||||
this
|
||||
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,
|
||||
|
|
Loading…
Reference in a new issue