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.material.icons)
implementation(compose.animation) implementation(compose.animation)
implementation(compose.animation.graphics) implementation(compose.animation.graphics)
implementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions) 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. * 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), title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title), 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), web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) }, summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) }, writer = manga.author?.let { ComicInfo.Writer(it) },
@ -108,6 +114,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo
publishingStatus = ComicInfo.PublishingStatusTachiyomi( publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status), ComicInfoPublishingStatus.toComicInfoValue(manga.status),
), ),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
inker = null, inker = null,
colorist = null, colorist = null,
letterer = null, letterer = null,

View file

@ -415,7 +415,7 @@ fun NsfwWarningDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onClickConfirm) { TextButton(onClick = onClickConfirm) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
onDismissRequest = onClickConfirm, 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() onDismissRequest()
}, },
) { ) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {
@ -147,7 +147,7 @@ fun CategoryDeleteDialog(
onDelete() onDelete()
onDismissRequest() onDismissRequest()
},) { },) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {

View file

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

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@ fun DeleteLibraryEntryDialog(
) )
}, },
) { ) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
title = { 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 = { 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 = { dismissButton = {

View file

@ -411,7 +411,7 @@ object SettingsAdvancedScreen : SearchableSettings {
uriHandler.openUri("https://shizuku.rikka.app/download") 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) onConfirm(flag)
}, },
) { ) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
) )
@ -266,7 +266,7 @@ object SettingsBackupScreen : SearchableSettings {
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onDismissRequest) { 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 = { confirmButton = {
TextButton(onClick = { onValueChanged(leadValue, followValue) }) { 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 = { confirmButton = {
TextButton(onClick = { onValueChanged(newLength) }) { 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), 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( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(), pref = readerPreferences.zoomStart(),
title = stringResource(R.string.pref_zoom_start), title = stringResource(R.string.pref_zoom_start),
@ -214,6 +209,11 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPreferences.cropBorders(), pref = readerPreferences.cropBorders(),
title = stringResource(R.string.pref_crop_borders), 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( Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.navigateToPan(), pref = readerPreferences.navigateToPan(),
title = stringResource(R.string.pref_navigate_pan), title = stringResource(R.string.pref_navigate_pan),

View file

@ -113,7 +113,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
tween(200) tween(200)
}, },
) )
then(Modifier.background(color = highlight)) Modifier.background(color = highlight)
} }
internal val TrailingWidgetBuffer = 16.dp 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 = { dismissButton = {

View file

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

View file

@ -137,7 +137,7 @@ fun <T> TriStateListDialog(
onValueChanged(included, excluded) 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 = { content = {
WheelTextPicker( WheelTextPicker(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
startIndex = selections.indexOf(selection).coerceAtLeast(0), startIndex = selections.indexOf(selection).takeIf { it > 0 } ?: (selections.size / 2),
items = selections, items = selections,
onSelectionChanged = { onSelectionChange(selections[it]) }, onSelectionChanged = { onSelectionChange(selections[it]) },
) )
@ -177,7 +177,7 @@ fun TrackDateSelector(
Text(text = stringResource(android.R.string.cancel)) Text(text = stringResource(android.R.string.cancel))
} }
TextButton(onClick = { onConfirm(pickerState.selectedDateMillis!!) }) { 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)) Text(text = stringResource(android.R.string.cancel))
} }
TextButton(onClick = onConfirm) { 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() onConfirm()
onDismissRequest() onDismissRequest()
},) { },) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(R.string.action_ok))
} }
}, },
dismissButton = { dismissButton = {

View file

@ -28,25 +28,11 @@ import java.io.IOException
*/ */
class ChapterCache(private val context: Context) { 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() private val json: Json by injectLazy()
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open( private val diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), File(context.cacheDir, "chapter_disk_cache"),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE, PARAMETER_CACHE_SIZE,
@ -55,8 +41,7 @@ class ChapterCache(private val context: Context) {
/** /**
* Returns directory of cache. * Returns directory of cache.
*/ */
private val cacheDir: File private val cacheDir: File = diskCache.directory
get() = diskCache.directory
/** /**
* Returns real size of directory. * Returns real size of directory.
@ -210,3 +195,12 @@ class ChapterCache(private val context: Context) {
return "${chapter.mangaId}${chapter.url}" 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) { 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() private val json: Json by injectLazy()
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open( private val diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), File(context.cacheDir, "episode_disk_cache"),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE, PARAMETER_CACHE_SIZE,
@ -54,8 +40,7 @@ class EpisodeCache(private val context: Context) {
/** /**
* Returns directory of cache. * Returns directory of cache.
*/ */
val cacheDir: File val cacheDir: File = diskCache.directory
get() = diskCache.directory
/** /**
* Returns real size of directory. * Returns real size of directory.
@ -193,3 +178,12 @@ class EpisodeCache(private val context: Context) {
return "${episode.animeId}${episode.url}" 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.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
@ -82,6 +83,7 @@ class MangaDownloader(
private val chapterCache: ChapterCache = Injekt.get(), private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val xml: XML = Injekt.get(), private val xml: XML = Injekt.get(),
private val getCategories: GetMangaCategories = Injekt.get(),
// SY --> // SY -->
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
// SY <-- // SY <--
@ -634,14 +636,15 @@ class MangaDownloader(
/** /**
* Creates a ComicInfo.xml file inside the given directory. * Creates a ComicInfo.xml file inside the given directory.
*/ */
private fun createComicInfoFile( private suspend fun createComicInfoFile(
dir: UniFile, dir: UniFile,
manga: Manga, manga: Manga,
chapter: Chapter, chapter: Chapter,
source: HttpSource, source: HttpSource,
) { ) {
val chapterUrl = source.getChapterUrl(chapter.toSChapter()) 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 // Remove the old file
dir.findFile(COMIC_INFO_FILE)?.delete() dir.findFile(COMIC_INFO_FILE)?.delete()
dir.createFile(COMIC_INFO_FILE).openOutputStream().use { 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.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService 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.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch 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.anime.model.AnimeTrack as DomainAnimeTrack
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack 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 { companion object {
const val READING = 1 const val READING = 1
@ -238,6 +240,24 @@ class Anilist(id: Long) : TrackService(id), MangaTrackService, AnimeTrackService
return api.updateLibAnime(track) 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 { override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUsername().toInt()) val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) { 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 { suspend fun addLibAnime(track: AnimeTrack): AnimeTrack {
return withIOContext { return withIOContext {
val query = """ 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> { suspend fun search(search: String): List<MangaTrackSearch> {
return withIOContext { return withIOContext {
val query = """ 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.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService 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.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -16,7 +18,7 @@ import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService { class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService, DeletableMangaTrackService, DeletableAnimeTrackService {
companion object { companion object {
const val READING = 1 const val READING = 1
@ -136,6 +138,14 @@ class Kitsu(id: Long) : TrackService(id), AnimeTrackService, MangaTrackService {
return api.updateLibAnime(track) 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 { override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUserId()) val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) { 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.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess 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> { suspend fun search(query: String): List<MangaTrackSearch> {
return withIOContext { return withIOContext {
with(json) { with(json) {

View file

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

View file

@ -106,6 +106,19 @@ class MangaUpdatesApi(
updateSeriesRating(track) 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? { private suspend fun getSeriesRating(track: MangaTrack): Rating? {
return try { return try {
with(json) { 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.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService 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.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -15,7 +17,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy 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 { companion object {
const val READING = 1 const val READING = 1
@ -140,6 +142,14 @@ class MyAnimeList(id: Long) : TrackService(id), MangaTrackService, AnimeTrackSer
return api.updateItem(track) 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 { override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findListItem(track) val remoteTrack = api.findListItem(track)
return if (remoteTrack != null) { 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? { suspend fun findListItem(track: MangaTrack): MangaTrack? {
return withIOContext { return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon() 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.anime.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
import eu.kanade.tachiyomi.data.track.AnimeTrackService 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.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
@ -15,7 +17,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy 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 { companion object {
const val READING = 1 const val READING = 1
@ -87,6 +89,14 @@ class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackServi
return api.updateLibAnime(track, getUsername()) 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 { override suspend fun bind(track: MangaTrack, hasReadChapters: Boolean): MangaTrack {
val remoteTrack = api.findLibManga(track, getUsername()) val remoteTrack = api.findLibManga(track, getUsername())
return if (remoteTrack != null) { return if (remoteTrack != null) {
@ -137,6 +147,7 @@ class Shikimori(id: Long) : TrackService(id), MangaTrackService, AnimeTrackServi
override suspend fun refresh(track: MangaTrack): MangaTrack { override suspend fun refresh(track: MangaTrack): MangaTrack {
api.findLibManga(track, getUsername())?.let { remoteTrack -> api.findLibManga(track, getUsername())?.let { remoteTrack ->
track.library_id = remoteTrack.library_id
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters 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 { override suspend fun refresh(track: AnimeTrack): AnimeTrack {
api.findLibAnime(track, getUsername())?.let { remoteTrack -> api.findLibAnime(track, getUsername())?.let { remoteTrack ->
track.library_id = remoteTrack.library_id
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.total_episodes = remoteTrack.total_episodes 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.TrackManager
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess 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 { suspend fun addLibManga(track: MangaTrack, user_id: String): MangaTrack {
return withIOContext { return withIOContext {
val payload = buildJsonObject { with(json) {
putJsonObject("user_rate") { val payload = buildJsonObject {
put("user_id", user_id) putJsonObject("user_rate") {
put("target_id", track.media_id) put("user_id", user_id)
put("target_type", "Manga") put("target_id", track.media_id)
put("chapters", track.last_chapter_read.toInt()) put("target_type", "Manga")
put("score", track.score.toInt()) put("chapters", track.last_chapter_read.toInt())
put("status", track.toShikimoriStatus()) 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( authClient.newCall(
POST( DELETE(
"$apiUrl/v2/user_rates", "$apiUrl/v2/user_rates/${track.library_id}",
body = payload.toString().toRequestBody(jsonMime),
), ),
).awaitSuccess() ).awaitSuccess()
track track
@ -59,30 +79,45 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
suspend fun addLibAnime(track: AnimeTrack, user_id: String): AnimeTrack { suspend fun addLibAnime(track: AnimeTrack, user_id: String): AnimeTrack {
return withIOContext { return withIOContext {
val payload = buildJsonObject { with(json) {
putJsonObject("user_rate") { val payload = buildJsonObject {
put("user_id", user_id) putJsonObject("user_rate") {
put("target_id", track.media_id) put("user_id", user_id)
put("target_type", "Manga") put("target_id", track.media_id)
put("chapters", track.last_episode_seen.toInt()) put("target_type", "Anime")
put("score", track.score.toInt()) put("chapters", track.last_episode_seen.toInt())
put("status", track.toShikimoriStatus()) 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( authClient.newCall(
POST( DELETE(
"$apiUrl/v2/user_rates", "$apiUrl/v2/user_rates/${track.library_id}",
body = payload.toString().toRequestBody(jsonMime),
), ),
).awaitSuccess() ).awaitSuccess()
track 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> { suspend fun search(search: String): List<MangaTrackSearch> {
return withIOContext { return withIOContext {
val url = "$apiUrl/mangas".toUri().buildUpon() val url = "$apiUrl/mangas".toUri().buildUpon()
@ -156,6 +191,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
title = mangas["name"]!!.jsonPrimitive.content title = mangas["name"]!!.jsonPrimitive.content
media_id = obj["id"]!!.jsonPrimitive.long media_id = obj["id"]!!.jsonPrimitive.long
total_chapters = mangas["chapters"]!!.jsonPrimitive.int total_chapters = mangas["chapters"]!!.jsonPrimitive.int
library_id = obj["id"]!!.jsonPrimitive.long
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
score = (obj["score"]!!.jsonPrimitive.int).toFloat() score = (obj["score"]!!.jsonPrimitive.int).toFloat()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
@ -168,6 +204,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
title = animes["name"]!!.jsonPrimitive.content title = animes["name"]!!.jsonPrimitive.content
media_id = obj["id"]!!.jsonPrimitive.long media_id = obj["id"]!!.jsonPrimitive.long
total_episodes = animes["episodes"]!!.jsonPrimitive.int total_episodes = animes["episodes"]!!.jsonPrimitive.int
library_id = obj["id"]!!.jsonPrimitive.long
last_episode_seen = obj["episodes"]!!.jsonPrimitive.float last_episode_seen = obj["episodes"]!!.jsonPrimitive.float
score = (obj["score"]!!.jsonPrimitive.int).toFloat() score = (obj["score"]!!.jsonPrimitive.int).toFloat()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) 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.app.Application
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth 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.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.presentation.util.Screen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.AnimeTrackService 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.EnhancedAnimeTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
@ -158,7 +161,16 @@ data class AnimeTrackInfoDialogHomeScreen(
} }
}, },
onOpenInBrowser = { openTrackerInBrowser(context, it) }, 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 animeId: Long,
private val sourceId: Long, private val sourceId: Long,
private val getTracks: GetAnimeTracks = Injekt.get(), private val getTracks: GetAnimeTracks = Injekt.get(),
private val deleteTrack: DeleteAnimeTrack = Injekt.get(),
) : StateScreenModel<Model.State>(State()) { ) : StateScreenModel<Model.State>(State()) {
init { init {
@ -205,10 +216,6 @@ data class AnimeTrackInfoDialogHomeScreen(
} }
} }
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(animeId, serviceId) }
}
private suspend fun refreshTrackers() { private suspend fun refreshTrackers() {
val insertAnimeTrack = Injekt.get<InsertAnimeTrack>() val insertAnimeTrack = Injekt.get<InsertAnimeTrack>()
val getAnimeWithEpisodes = Injekt.get<GetAnimeWithEpisodes>() 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.app.Application
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth 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.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.track.manga.MangaTrackServiceSearch
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R 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.EnhancedMangaTrackService
import eu.kanade.tachiyomi.data.track.MangaTrackService import eu.kanade.tachiyomi.data.track.MangaTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@ -158,7 +161,16 @@ data class MangaTrackInfoDialogHomeScreen(
} }
}, },
onOpenInBrowser = { openTrackerInBrowser(context, it) }, 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 mangaId: Long,
private val sourceId: Long, private val sourceId: Long,
private val getTracks: GetMangaTracks = Injekt.get(), private val getTracks: GetMangaTracks = Injekt.get(),
private val deleteTrack: DeleteMangaTrack = Injekt.get(),
) : StateScreenModel<Model.State>(State()) { ) : StateScreenModel<Model.State>(State()) {
init { init {
@ -205,10 +216,6 @@ data class MangaTrackInfoDialogHomeScreen(
} }
} }
fun unregisterTracking(serviceId: Long) {
coroutineScope.launchNonCancellable { deleteTrack.await(mangaId, serviceId) }
}
private suspend fun refreshTrackers() { private suspend fun refreshTrackers() {
val insertTrack = Injekt.get<InsertMangaTrack>() val insertTrack = Injekt.get<InsertMangaTrack>()
val getMangaWithChapters = Injekt.get<GetMangaWithChapters>() 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 = { confirmButton = {
TextButton(onClick = { showChangelog = false }) { 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 eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.TextButton 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" // "R.string.action_ok"
@Composable @Composable
@ -71,7 +71,7 @@ fun PlayerDialog(
} }
TextButton(onClick = onConfirm) { 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 // Init listeners on bottom menu
binding.readerNav.setComposeContent { binding.readerNav.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
@ -790,7 +804,7 @@ class ReaderActivity : BaseActivity() {
* actions to perform is shown. * actions to perform is shown.
*/ */
fun onPageLongTap(page: ReaderPage) { 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 * Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool. * sharing tool.
@ -850,14 +856,6 @@ class ReaderActivity : BaseActivity() {
startActivity(Intent.createChooser(intent, getString(R.string.action_share))) 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 * Called from the presenter when a page is saved or fails. It shows a message or logs the
* event depending on the [result]. * 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 * Called from the presenter when a page is set as cover or fails. It shows a different message
* depending on the [result]. * 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 ) + 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. * There's also a notification to allow sharing the image somewhere else or deleting it.
*/ */
fun saveImage(page: ReaderPage) { fun saveImage() {
if (page.status != Page.State.READY) return val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() 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 * 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 * 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 * 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. * image will be kept so it won't be taking lots of internal disk space.
*/ */
fun shareImage(page: ReaderPage) { fun shareImage() {
if (page.status != Page.State.READY) return val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() 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) { fun setAsCover() {
if (page.status != Page.State.READY) return val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return val manga = manga ?: return
val stream = page.stream ?: return val stream = page.stream ?: return
@ -906,11 +917,16 @@ class ReaderViewModel(
* Viewer used to display the pages (pager, webtoon, ...). * Viewer used to display the pages (pager, webtoon, ...).
*/ */
val viewer: Viewer? = null, val viewer: Viewer? = null,
val dialog: Dialog? = null,
) { ) {
val totalPages: Int val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1 get() = viewerChapters?.currChapter?.pages?.size ?: -1
} }
sealed class Dialog {
data class Page(val page: ReaderPage) : Dialog()
}
sealed class Event { sealed class Event {
object ReloadViewerChapters : Event() object ReloadViewerChapters : Event()
data class SetOrientation(val orientation: Int) : Event() data class SetOrientation(val orientation: Int) : Event()

View file

@ -138,4 +138,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout> </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" android:entries="@array/image_scale_type"
app:title="@string/pref_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 <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/zoom_start" android:id="@+id/zoom_start"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -63,6 +54,15 @@
android:text="@string/pref_crop_borders" android:text="@string/pref_crop_borders"
android:textColor="?android:attr/textColorSecondary" /> 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 <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/navigate_pan" android:id="@+id/navigate_pan"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

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

View file

@ -17,6 +17,6 @@ class NetworkPreferences(
} }
fun defaultUserAgent(): Preference<String> { 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 = { module = "androidx.compose.animation:animation" }
animation-graphics = { module = "androidx.compose.animation:animation-graphics" } animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 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" } ui-util = { module = "androidx.compose.ui:ui-util" }
material3-core = { module = "androidx.compose.material3:material3" } material3-core = { module = "androidx.compose.material3:material3" }

View file

@ -121,6 +121,7 @@
<string name="action_pin">Pin</string> <string name="action_pin">Pin</string>
<string name="action_unpin">Unpin</string> <string name="action_unpin">Unpin</string>
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_ok">OK</string>
<string name="action_cancel_all">Cancel all</string> <string name="action_cancel_all">Cancel all</string>
<string name="cancel_all_for_series">Cancel all for this series</string> <string name="cancel_all_for_series">Cancel all for this series</string>
<string name="action_sort">Sort</string> <string name="action_sort">Sort</string>
@ -404,7 +405,7 @@
<string name="scale_type_original_size">Original size</string> <string name="scale_type_original_size">Original size</string>
<string name="scale_type_smart_fit">Smart fit</string> <string name="scale_type_smart_fit">Smart fit</string>
<string name="pref_navigate_pan">Pan wide images</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="pref_zoom_start">Zoom start position</string>
<string name="zoom_start_automatic">Automatic</string> <string name="zoom_start_automatic">Automatic</string>
<string name="zoom_start_left">Left</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_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_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_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 --> <!-- Category activity -->
<string name="error_category_exists">A category with this name already exists!</string> <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.material.icons)
implementation(compose.animation) implementation(compose.animation)
implementation(compose.animation.graphics) implementation(compose.animation.graphics)
implementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) 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,20 +30,21 @@ import tachiyomi.presentation.core.components.Pill
private fun Modifier.tabIndicatorOffset( private fun Modifier.tabIndicatorOffset(
currentTabPosition: TabPosition, currentTabPosition: TabPosition,
currentPageOffsetFraction: Float, currentPageOffsetFraction: Float,
) = composed { ) = fillMaxWidth()
val currentTabWidth by animateDpAsState( .wrapContentSize(Alignment.BottomStart)
targetValue = currentTabPosition.width, .composed {
animationSpec = spring(stiffness = Spring.StiffnessMediumLow), val currentTabWidth by animateDpAsState(
) targetValue = currentTabPosition.width,
val offset by animateDpAsState( animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction), )
animationSpec = spring(stiffness = Spring.StiffnessMediumLow), val offset by animateDpAsState(
) targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction),
fillMaxWidth() animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
.wrapContentSize(Alignment.BottomStart) )
.offset { IntOffset(x = offset.roundToPx(), y = 0) } Modifier
.width(currentTabWidth) .offset { IntOffset(x = offset.roundToPx(), y = 0) }
} .width(currentTabWidth)
}
@Composable @Composable
fun TabIndicator( fun TabIndicator(

View file

@ -4,17 +4,13 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.random.Random 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( private val ERROR_FACES = listOf(
"(・o・;)", "(・o・;)",
"Σ(ಠ_ಠ)", "Σ(ಠ_ಠ)",

View file

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