mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 13:48:55 +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.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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
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 = {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -28,7 +28,7 @@ fun DeleteItemsDialog(
|
||||||
onConfirm()
|
onConfirm()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
|
|
|
@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -212,7 +212,7 @@ private fun SetAsDefaultDialog(
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -56,7 +56,7 @@ fun DeleteLibraryEntryDialog(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
|
|
|
@ -83,7 +83,7 @@ class ClearAnimeDatabaseScreen : Screen() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
|
@ -83,7 +83,7 @@ class ClearDatabaseScreen : Screen() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -84,7 +84,7 @@ fun EditTextPreferenceWidget(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.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) {
|
||||||
|
|
|
@ -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 = """
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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
|
) + 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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
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(
|
||||||
|
|
|
@ -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・;)",
|
||||||
"Σ(ಠ_ಠ)",
|
"Σ(ಠ_ಠ)",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue