Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 129841d5c2 | |||
| 24d2460697 | |||
| ef3d9626c1 | |||
| 5c7b3c6c3b | |||
| 6257888735 | |||
| f5e714f794 | |||
| 3091f63504 | |||
| 39755cccdc | |||
| 9caf706ca3 | |||
| 6ba6a7c8d9 | |||
| 0a4a0e4c4c | |||
| b48d1e521a | |||
| 211d090a2d | |||
| b6e5943e15 | |||
| 78f6a34339 | |||
| de967ae149 | |||
| 4d075ff190 | |||
| 076e2961c6 | |||
| 7149de1bc3 | |||
| 091f2f583a | |||
| 1c0ef2ca98 | |||
| 2a845bd7b5 | |||
| afe326006f | |||
| 4b80154b09 | |||
| d6b230b8f1 | |||
| d02a2cbd29 | |||
| 17d225b0d9 | |||
| 6cbbaccaf4 | |||
| 94cc4027c2 | |||
| 03ae6ed2b0 | |||
| fa8c232a69 | |||
| 0386ce998a | |||
| 273f73e9a2 | |||
| 5e20e54649 | |||
| b8c3f9dcce | |||
| 802b6508fa | |||
| b6409b05e7 | |||
| 129f355b9c | |||
| 9ffacb80e3 | |||
| 85726db45d | |||
| 746b1b051c | |||
| 59887eed80 | |||
| b8267f1fef | |||
| 8c62bb6d6d | |||
| 751e04b87f | |||
| 9f0161ed70 | |||
| 7b2c341386 | |||
| c8b29ecf1c | |||
| c30381c6ec | |||
| f489531543 | |||
| 4bbe795626 | |||
| 8aa3dca95f | |||
| 5e0f730159 | |||
| f1aed0d8b9 | |||
| a3465c31c9 | |||
| 053c48613b | |||
| 615adc567b | |||
| b0f645d906 | |||
| 023c78d0e8 | |||
| 824550175a | |||
| ad53c0de83 | |||
| c8039739d5 | |||
| 26674136e6 | |||
| 9972fa1053 | |||
| ae3f974d8c | |||
| 027f179a4b | |||
| e80cb1795e | |||
| 66fe599498 | |||
| c9e6e321b3 | |||
| fb3c996904 | |||
| 70b25825ec | |||
| 290e8f5a1e | |||
| f6b1440bf2 | |||
| 77a4919656 | |||
| 84d901b8a3 | |||
| d27ed2580f | |||
| 87ea971be0 | |||
| 91ea70b335 | |||
| 2e94e152c2 | |||
| eece46fb0f | |||
| 34736bc26e | |||
| 82cf385f9d | |||
| 682dea2fb1 | |||
| c10588d183 |
+1
-1
@@ -7,7 +7,7 @@ indent_style = space
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.xml]
|
[*.{xml,sq,sqm}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v2.6.0
|
uses: tachiyomiorg/issue-moderator-action@v2.6.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
duplicate-label: Duplicate
|
duplicate-label: Duplicate
|
||||||
|
|||||||
+7
-12
@@ -31,12 +31,12 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
|
|
||||||
versionCode = 73
|
versionCode = 74
|
||||||
versionName = "1.12.0"
|
versionName = "1.12.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
@@ -71,6 +71,8 @@ android {
|
|||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
|
||||||
|
|
||||||
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
|
||||||
}
|
}
|
||||||
create("benchmark") {
|
create("benchmark") {
|
||||||
initWith(getByName("release"))
|
initWith(getByName("release"))
|
||||||
@@ -237,7 +239,7 @@ dependencies {
|
|||||||
implementation(libs.preferencektx)
|
implementation(libs.preferencektx)
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(platform(libs.coil.bom))
|
implementation(platform(libs.coil.bom))
|
||||||
@@ -255,7 +257,7 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.bundles.richtext)
|
implementation(libs.richeditor.compose)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.compose.materialmotion)
|
||||||
@@ -263,6 +265,7 @@ dependencies {
|
|||||||
implementation(libs.compose.webview)
|
implementation(libs.compose.webview)
|
||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
implementation(libs.bundles.markdown)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
@@ -313,14 +316,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
beforeVariants { variantBuilder ->
|
|
||||||
// Disables standardBenchmark
|
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
|
||||||
listOf("default" to "dev"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
// Only excluding in standard flavor because this breaks
|
// Only excluding in standard flavor because this breaks
|
||||||
// Layout Inspector's Compose tree
|
// Layout Inspector's Compose tree
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
|||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
|
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.domain.release.service.ReleaseService
|
import tachiyomi.domain.release.service.ReleaseService
|
||||||
@@ -128,6 +129,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get(), get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
|
addFactory { UpdateMangaNotes(get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
addFactory { GetExcludedScanlators(get()) }
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
addFactory { SetExcludedScanlators(get()) }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import eu.kanade.domain.manga.model.hasCustomCover
|
|||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
@@ -32,9 +33,8 @@ class UpdateManga(
|
|||||||
remoteManga: SManga,
|
remoteManga: SManga,
|
||||||
manualFetch: Boolean,
|
manualFetch: Boolean,
|
||||||
coverCache: CoverCache = Injekt.get(),
|
coverCache: CoverCache = Injekt.get(),
|
||||||
// SY -->
|
libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
downloadManager: DownloadManager = Injekt.get(),
|
downloadManager: DownloadManager = Injekt.get(),
|
||||||
// SY <--
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val remoteTitle = try {
|
val remoteTitle = try {
|
||||||
remoteManga.title
|
remoteManga.title
|
||||||
@@ -42,14 +42,13 @@ class UpdateManga(
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
|
||||||
val title = if (remoteTitle.isNotBlank() && localManga.ogTitle != remoteTitle) {
|
val title =
|
||||||
downloadManager.renameMangaDir(localManga.ogTitle, remoteTitle, localManga.source)
|
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
|
||||||
remoteTitle
|
remoteTitle
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
// SY <--
|
|
||||||
|
|
||||||
val coverLastModified =
|
val coverLastModified =
|
||||||
when {
|
when {
|
||||||
@@ -69,7 +68,7 @@ class UpdateManga(
|
|||||||
|
|
||||||
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
return mangaRepository.update(
|
val success = mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
id = localManga.id,
|
id = localManga.id,
|
||||||
title = title,
|
title = title,
|
||||||
@@ -84,6 +83,10 @@ class UpdateManga(
|
|||||||
initialized = true,
|
initialized = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (success && title != null) {
|
||||||
|
downloadManager.renameManga(localManga, title)
|
||||||
|
}
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
|
|||||||
@@ -76,24 +76,6 @@ fun Manga.copyFrom(other: SManga): Manga {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SManga.toDomainManga(sourceId: Long): Manga {
|
|
||||||
return Manga.create().copy(
|
|
||||||
url = url,
|
|
||||||
// SY -->
|
|
||||||
ogTitle = title,
|
|
||||||
ogArtist = artist,
|
|
||||||
ogAuthor = author,
|
|
||||||
ogThumbnailUrl = thumbnail_url,
|
|
||||||
ogDescription = description,
|
|
||||||
ogGenre = getGenres(),
|
|
||||||
ogStatus = status.toLong(),
|
|
||||||
// SY <--
|
|
||||||
updateStrategy = update_strategy,
|
|
||||||
initialized = initialized,
|
|
||||||
source = sourceId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||||
return coverCache.getCustomCoverFile(id).exists()
|
return coverCache.getCustomCoverFile(id).exists()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package eu.kanade.domain.ui.model
|
package eu.kanade.domain.ui.model
|
||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
enum class AppTheme(val titleRes: StringResource?) {
|
enum class AppTheme(val titleRes: StringResource?) {
|
||||||
@@ -11,9 +9,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
|||||||
GREEN_APPLE(MR.strings.theme_greenapple),
|
GREEN_APPLE(MR.strings.theme_greenapple),
|
||||||
LAVENDER(MR.strings.theme_lavender),
|
LAVENDER(MR.strings.theme_lavender),
|
||||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||||
|
NORD(MR.strings.theme_nord),
|
||||||
// TODO: re-enable for preview
|
|
||||||
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
|
||||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||||
TAKO(MR.strings.theme_tako),
|
TAKO(MR.strings.theme_tako),
|
||||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||||
|
|||||||
@@ -82,10 +82,18 @@ fun BrowseSourceContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||||
|
LoadingScreen(Modifier.padding(contentPadding))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mangaList.itemCount == 0) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
message = getErrorMessage(errorState),
|
message = when (errorState) {
|
||||||
|
is LoadState.Error -> getErrorMessage(errorState)
|
||||||
|
else -> stringResource(MR.strings.no_results_found)
|
||||||
|
},
|
||||||
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
|
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
@@ -104,7 +112,7 @@ fun BrowseSourceContent(
|
|||||||
// SY -->
|
// SY -->
|
||||||
if (onWebViewClick != null) {
|
if (onWebViewClick != null) {
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
MR.strings.action_open_in_web_view,
|
stringRes = MR.strings.action_open_in_web_view,
|
||||||
icon = Icons.Outlined.Public,
|
icon = Icons.Outlined.Public,
|
||||||
onClick = onWebViewClick,
|
onClick = onWebViewClick,
|
||||||
)
|
)
|
||||||
@@ -113,7 +121,7 @@ fun BrowseSourceContent(
|
|||||||
},
|
},
|
||||||
if (onHelpClick != null) {
|
if (onHelpClick != null) {
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
MR.strings.label_help,
|
stringRes = MR.strings.label_help,
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||||
onClick = onHelpClick,
|
onClick = onHelpClick,
|
||||||
)
|
)
|
||||||
@@ -128,13 +136,6 @@ fun BrowseSourceContent(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
|
||||||
LoadingScreen(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
|
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
|
||||||
BrowseSourceEHentaiList(
|
BrowseSourceEHentaiList(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.compose.animation.SizeTransform
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -28,20 +27,14 @@ fun NavigatorAdaptiveSheet(
|
|||||||
screen = screen,
|
screen = screen,
|
||||||
content = { sheetNavigator ->
|
content = { sheetNavigator ->
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||||
) {
|
) {
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = sheetNavigator,
|
navigator = sheetNavigator,
|
||||||
transition = {
|
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
|
||||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
exitTransition = { fadeOut(animationSpec = tween(90)) },
|
||||||
fadeOut(animationSpec = tween(90))
|
sizeTransform = { SizeTransform() },
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
BackHandler(
|
|
||||||
enabled = sheetNavigator.size > 1,
|
|
||||||
onBack = sheetNavigator::pop,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +72,10 @@ fun AdaptiveSheet(
|
|||||||
properties = dialogProperties,
|
properties = dialogProperties,
|
||||||
) {
|
) {
|
||||||
AdaptiveSheetImpl(
|
AdaptiveSheetImpl(
|
||||||
modifier = modifier,
|
|
||||||
isTabletUi = isTabletUi,
|
isTabletUi = isTabletUi,
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -201,6 +202,7 @@ fun AppBarActions(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = it.onClick,
|
onClick = it.onClick,
|
||||||
@@ -225,6 +227,7 @@ fun AppBarActions(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showMenu = !showMenu },
|
onClick = { showMenu = !showMenu },
|
||||||
@@ -289,6 +292,7 @@ fun SearchToolbar(
|
|||||||
onSearch(searchQuery)
|
onSearch(searchQuery)
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
focusManager.moveFocus(FocusDirection.Next)
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
@@ -352,6 +356,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -371,6 +376,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state = rememberTooltipState(),
|
state = rememberTooltipState(),
|
||||||
|
focusable = false,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -1,44 +1,95 @@
|
|||||||
package eu.kanade.presentation.manga
|
package eu.kanade.presentation.manga
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Brush
|
||||||
|
import androidx.compose.material.icons.filled.PersonOutline
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.outlined.Book
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.SwapVert
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Done
|
||||||
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
|
import androidx.compose.material.icons.outlined.Pause
|
||||||
|
import androidx.compose.material.icons.outlined.Schedule
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.util.fastMaxOfOrNull
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
import tachiyomi.presentation.core.components.BadgeGroup
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DuplicateMangaDialog(
|
fun DuplicateMangaDialog(
|
||||||
|
duplicates: List<MangaWithChapterCount>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onOpenManga: () -> Unit,
|
onOpenManga: (manga: Manga) -> Unit,
|
||||||
onMigrate: () -> Unit,
|
onMigrate: (manga: Manga) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||||
val minHeight = LocalPreferenceMinHeight.current
|
val minHeight = LocalPreferenceMinHeight.current
|
||||||
|
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
|
||||||
|
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -46,81 +97,310 @@ fun DuplicateMangaDialog(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||||
vertical = TabbedDialogPaddings.Vertical,
|
.verticalScroll(rememberScrollState())
|
||||||
horizontal = TabbedDialogPaddings.Horizontal,
|
|
||||||
)
|
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(TitlePadding),
|
text = stringResource(MR.strings.possible_duplicates_title),
|
||||||
text = stringResource(MR.strings.are_you_sure),
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(horizontalPaddingModifier)
|
||||||
|
.padding(top = MaterialTheme.padding.small),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.confirm_add_duplicate_manga),
|
text = stringResource(MR.strings.possible_duplicates_summary),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.then(horizontalPaddingModifier),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(PaddingSize))
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
TextPreferenceWidget(
|
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
|
||||||
title = stringResource(MR.strings.action_show_manga),
|
contentPadding = horizontalPadding,
|
||||||
icon = Icons.Outlined.Book,
|
|
||||||
onPreferenceClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onOpenManga()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
|
||||||
|
|
||||||
TextPreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.action_migrate_duplicate),
|
|
||||||
icon = Icons.Outlined.SwapVert,
|
|
||||||
onPreferenceClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onMigrate()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
|
||||||
|
|
||||||
TextPreferenceWidget(
|
|
||||||
title = stringResource(MR.strings.action_add_anyway),
|
|
||||||
icon = Icons.Outlined.Add,
|
|
||||||
onPreferenceClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onConfirm()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(minHeight = minHeight)
|
|
||||||
.clickable { onDismissRequest.invoke() }
|
|
||||||
.padding(ButtonPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
) {
|
||||||
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
|
items(
|
||||||
Text(
|
items = duplicates,
|
||||||
modifier = Modifier
|
key = { it.manga.id },
|
||||||
.padding(vertical = 8.dp),
|
) {
|
||||||
text = stringResource(MR.strings.action_cancel),
|
DuplicateMangaListItem(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
duplicate = it,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
getSource = { sourceManager.getOrStub(it.manga.source) },
|
||||||
fontSize = 16.sp,
|
onMigrate = { onMigrate(it.manga) },
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onOpenManga = { onOpenManga(it.manga) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(modifier = horizontalPaddingModifier) {
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.action_add_anyway),
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
onPreferenceClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onConfirm()
|
||||||
|
},
|
||||||
|
modifier = Modifier.clip(CircleShape),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onDismissRequest,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(horizontalPaddingModifier)
|
||||||
|
.padding(bottom = MaterialTheme.padding.medium)
|
||||||
|
.heightIn(min = minHeight)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
|
||||||
|
text = stringResource(MR.strings.action_cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val PaddingSize = 16.dp
|
@Composable
|
||||||
|
private fun DuplicateMangaListItem(
|
||||||
|
duplicate: MangaWithChapterCount,
|
||||||
|
getSource: () -> Source,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onOpenManga: () -> Unit,
|
||||||
|
onMigrate: () -> Unit,
|
||||||
|
) {
|
||||||
|
val source = getSource()
|
||||||
|
val manga = duplicate.manga
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(MangaCardWidth)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { onOpenManga() },
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onMigrate()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.padding(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
MangaCover.Book(
|
||||||
|
data = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(manga)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
BadgeGroup(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.align(Alignment.TopStart),
|
||||||
|
) {
|
||||||
|
Badge(
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textColor = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
text = pluralStringResource(
|
||||||
|
MR.plurals.manga_num_chapters,
|
||||||
|
duplicate.chapterCount.toInt(),
|
||||||
|
duplicate.chapterCount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
|
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
|
||||||
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
|
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!manga.author.isNullOrBlank()) {
|
||||||
|
MangaDetailRow(
|
||||||
|
text = manga.author!!,
|
||||||
|
iconImageVector = Icons.Filled.PersonOutline,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
|
||||||
|
MangaDetailRow(
|
||||||
|
text = manga.artist!!,
|
||||||
|
iconImageVector = Icons.Filled.Brush,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaDetailRow(
|
||||||
|
text = when (manga.status) {
|
||||||
|
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing)
|
||||||
|
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed)
|
||||||
|
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed)
|
||||||
|
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished)
|
||||||
|
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled)
|
||||||
|
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus)
|
||||||
|
else -> stringResource(MR.strings.unknown)
|
||||||
|
},
|
||||||
|
iconImageVector = when (manga.status) {
|
||||||
|
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||||
|
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||||
|
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||||
|
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||||
|
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||||
|
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||||
|
else -> Icons.Outlined.Block
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
if (source is StubSource) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = source.name,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MangaDetailRow(
|
||||||
|
text: String,
|
||||||
|
iconImageVector: ImageVector,
|
||||||
|
maxLines: Int = 1,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.secondaryItemAlpha()
|
||||||
|
.padding(top = MaterialTheme.padding.extraSmall),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = iconImageVector,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(MangaDetailsIconWidth),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = maxLines,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val textMeasurer = rememberTextMeasurer()
|
||||||
|
|
||||||
|
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
|
||||||
|
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
|
||||||
|
|
||||||
|
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
|
||||||
|
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
|
||||||
|
|
||||||
|
val coverHeight = width / MangaCover.Book.ratio
|
||||||
|
val constraints = Constraints(maxWidth = width)
|
||||||
|
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
|
||||||
|
|
||||||
|
return remember(
|
||||||
|
duplicates,
|
||||||
|
density,
|
||||||
|
typography,
|
||||||
|
textMeasurer,
|
||||||
|
smallPadding,
|
||||||
|
extraSmallPadding,
|
||||||
|
coverHeight,
|
||||||
|
constraints,
|
||||||
|
detailsConstraints,
|
||||||
|
) {
|
||||||
|
duplicates.fastMaxOfOrNull {
|
||||||
|
calculateMangaCardHeight(
|
||||||
|
manga = it.manga,
|
||||||
|
density = density,
|
||||||
|
typography = typography,
|
||||||
|
textMeasurer = textMeasurer,
|
||||||
|
smallPadding = smallPadding,
|
||||||
|
extraSmallPadding = extraSmallPadding,
|
||||||
|
coverHeight = coverHeight,
|
||||||
|
constraints = constraints,
|
||||||
|
detailsConstraints = detailsConstraints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: 0.dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateMangaCardHeight(
|
||||||
|
manga: Manga,
|
||||||
|
density: Density,
|
||||||
|
typography: Typography,
|
||||||
|
textMeasurer: TextMeasurer,
|
||||||
|
smallPadding: Int,
|
||||||
|
extraSmallPadding: Int,
|
||||||
|
coverHeight: Float,
|
||||||
|
constraints: Constraints,
|
||||||
|
detailsConstraints: Constraints,
|
||||||
|
): Dp {
|
||||||
|
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
|
||||||
|
val authorHeight = if (!manga.author.isNullOrBlank()) {
|
||||||
|
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
|
||||||
|
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
|
||||||
|
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
|
||||||
|
|
||||||
|
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
|
||||||
|
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TextMeasurer.measureHeight(
|
||||||
|
text: String,
|
||||||
|
style: TextStyle,
|
||||||
|
maxLines: Int,
|
||||||
|
constraints: Constraints,
|
||||||
|
): Int = measure(
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = maxLines,
|
||||||
|
constraints = constraints,
|
||||||
|
)
|
||||||
|
.size
|
||||||
|
.height
|
||||||
|
|
||||||
|
private val MangaCardWidth = 150.dp
|
||||||
|
private val MangaDetailsIconWidth = 16.dp
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.presentation.manga
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarTitle
|
||||||
|
import eu.kanade.presentation.manga.components.MangaNotesTextArea
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaNotesScreen(
|
||||||
|
state: MangaNotesScreen.State,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
onUpdate: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = { topBarScrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
titleContent = {
|
||||||
|
AppBarTitle(
|
||||||
|
title = stringResource(MR.strings.action_edit_notes),
|
||||||
|
subtitle = state.manga.title,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
scrollBehavior = topBarScrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
MangaNotesTextArea(
|
||||||
|
state = state,
|
||||||
|
onUpdate = onUpdate,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.consumeWindowInsets(contentPadding)
|
||||||
|
.imePadding(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditFetchIntervalClicked: (() -> Unit)?,
|
onEditFetchIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@@ -201,6 +202,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
onEditInfoClicked = onEditInfoClicked,
|
||||||
@@ -247,6 +249,7 @@ fun MangaScreen(
|
|||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
onEditIntervalClicked = onEditFetchIntervalClicked,
|
onEditIntervalClicked = onEditFetchIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
|
onEditNotesClicked = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||||
onEditInfoClicked = onEditInfoClicked,
|
onEditInfoClicked = onEditInfoClicked,
|
||||||
@@ -303,6 +306,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@@ -345,13 +349,9 @@ private fun MangaScreenSmallImpl(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
onAllChapterSelected(false)
|
||||||
onAllChapterSelected(false)
|
}
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -382,6 +382,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
||||||
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
||||||
@@ -519,8 +520,10 @@ private fun MangaScreenSmallImpl(
|
|||||||
defaultExpandState = state.isFromSource,
|
defaultExpandState = state.isFromSource,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
@@ -626,6 +629,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
onEditIntervalClicked: (() -> Unit)?,
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onMetadataViewerClicked: () -> Unit,
|
onMetadataViewerClicked: () -> Unit,
|
||||||
onEditInfoClicked: () -> Unit,
|
onEditInfoClicked: () -> Unit,
|
||||||
@@ -672,13 +676,9 @@ fun MangaScreenLargeImpl(
|
|||||||
|
|
||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
onAllChapterSelected(false)
|
||||||
onAllChapterSelected(false)
|
}
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -696,6 +696,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onClickEditCategory = onEditCategoryClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onClickRefresh = onRefresh,
|
onClickRefresh = onRefresh,
|
||||||
onClickMigrate = onMigrateClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
|
onClickEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
|
||||||
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
|
||||||
@@ -814,8 +815,10 @@ fun MangaScreenLargeImpl(
|
|||||||
defaultExpandState = true,
|
defaultExpandState = true,
|
||||||
description = state.manga.description,
|
description = state.manga.description,
|
||||||
tagsProvider = { state.manga.genre },
|
tagsProvider = { state.manga.genre },
|
||||||
|
notes = state.manga.notes,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = onCopyTagToClipboard,
|
onCopyTagToClipboard = onCopyTagToClipboard,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
// SY -->
|
// SY -->
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package eu.kanade.presentation.manga.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -25,18 +28,22 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
@@ -49,11 +56,14 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCoverDialog(
|
fun MangaCoverDialog(
|
||||||
@@ -152,10 +162,32 @@ fun MangaCoverDialog(
|
|||||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
PredictiveBackHandler { progress ->
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.8f, PredictiveBack.transform(backEvent.progress))
|
||||||
|
}
|
||||||
|
onDismissRequest()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickableNoIndication(onClick = onDismissRequest),
|
.clickableNoIndication(onClick = onDismissRequest)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
@@ -172,20 +204,20 @@ fun MangaCoverDialog(
|
|||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.target { image ->
|
.target { image ->
|
||||||
val drawable = image.asDrawable(view.context.resources)
|
val drawable = image.asDrawable(view.context.resources)
|
||||||
|
|
||||||
// Copy bitmap in case it came from memory cache
|
// Copy bitmap in case it came from memory cache
|
||||||
// Because SSIV needs to thoroughly read the image
|
// Because SSIV needs to thoroughly read the image
|
||||||
val copy = (drawable as? BitmapDrawable)?.let {
|
val copy = (drawable as? BitmapDrawable)
|
||||||
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
?.bitmap
|
||||||
Bitmap.Config.HARDWARE
|
?.copy(
|
||||||
} else {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.HARDWARE
|
||||||
}
|
} else {
|
||||||
BitmapDrawable(
|
Bitmap.Config.ARGB_8888
|
||||||
view.context.resources,
|
},
|
||||||
it.bitmap.copy(config, false),
|
false,
|
||||||
)
|
)
|
||||||
} ?: drawable
|
?.toDrawable(view.context.resources)
|
||||||
|
?: drawable
|
||||||
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotatorConfig
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -95,8 +97,6 @@ import java.time.Instant
|
|||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaInfoBox(
|
fun MangaInfoBox(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@@ -250,8 +250,10 @@ fun ExpandableMangaDescription(
|
|||||||
defaultExpandState: Boolean,
|
defaultExpandState: Boolean,
|
||||||
description: String?,
|
description: String?,
|
||||||
tagsProvider: () -> List<String>?,
|
tagsProvider: () -> List<String>?,
|
||||||
|
notes: String,
|
||||||
onTagSearch: (String) -> Unit,
|
onTagSearch: (String) -> Unit,
|
||||||
onCopyTagToClipboard: (tag: String) -> Unit,
|
onCopyTagToClipboard: (tag: String) -> Unit,
|
||||||
|
onEditNotes: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
searchMetadataChips: SearchMetadataChips?,
|
searchMetadataChips: SearchMetadataChips?,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
@@ -264,15 +266,12 @@ fun ExpandableMangaDescription(
|
|||||||
}
|
}
|
||||||
val desc =
|
val desc =
|
||||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
||||||
val trimmedDescription = remember(desc) {
|
|
||||||
desc
|
|
||||||
.replace(whitespaceLineRegex, "\n")
|
|
||||||
.trimEnd()
|
|
||||||
}
|
|
||||||
MangaSummary(
|
MangaSummary(
|
||||||
expandedDescription = desc,
|
description = desc,
|
||||||
shrunkDescription = trimmedDescription,
|
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
|
notes = notes,
|
||||||
|
onEditNotesClicked = onEditNotes,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -594,11 +593,26 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val descriptionAnnotator = markdownAnnotator(
|
||||||
|
annotate = { content, child ->
|
||||||
|
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||||
|
append(content.substring(child.startOffset, child.endOffset))
|
||||||
|
return@markdownAnnotator true
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
|
config = markdownAnnotatorConfig(
|
||||||
|
eolAsNewLine = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
expandedDescription: String,
|
description: String,
|
||||||
shrunkDescription: String,
|
notes: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
|
onEditNotesClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val animProgress by animateFloatAsState(
|
val animProgress by animateFloatAsState(
|
||||||
@@ -610,25 +624,40 @@ private fun MangaSummary(
|
|||||||
contents = listOf(
|
contents = listOf(
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
text = "\n\n", // Shows at least 3 lines
|
// Shows at least 3 lines if no notes
|
||||||
|
// when there are notes show 6
|
||||||
|
text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Text(
|
Column {
|
||||||
text = expandedDescription,
|
MangaNotesSection(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
content = notes,
|
||||||
)
|
expanded = true,
|
||||||
},
|
onEditNotes = onEditNotesClicked,
|
||||||
{
|
|
||||||
SelectionContainer {
|
|
||||||
Text(
|
|
||||||
text = if (expanded) expandedDescription else shrunkDescription,
|
|
||||||
maxLines = Int.MAX_VALUE,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
)
|
)
|
||||||
|
MarkdownRender(
|
||||||
|
content = description,
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
annotator = descriptionAnnotator,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Column {
|
||||||
|
MangaNotesSection(
|
||||||
|
content = notes,
|
||||||
|
expanded = expanded,
|
||||||
|
onEditNotes = onEditNotesClicked,
|
||||||
|
)
|
||||||
|
SelectionContainer {
|
||||||
|
MarkdownRender(
|
||||||
|
content = description,
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
annotator = descriptionAnnotator,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.draw.alpha
|
||||||
|
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||||
|
import com.mohamedrejeb.richeditor.ui.material3.RichText
|
||||||
|
|
||||||
|
private val FADE_TIME = tween<Float>(500)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaNotesDisplay(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val alpha = remember { Animatable(1f) }
|
||||||
|
var contentUpdatedOnce by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val richTextState = rememberRichTextState()
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
LaunchedEffect(content) {
|
||||||
|
richTextState.setMarkdown(content)
|
||||||
|
|
||||||
|
if (!contentUpdatedOnce) {
|
||||||
|
contentUpdatedOnce = true
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha.snapTo(targetValue = 0f)
|
||||||
|
alpha.animateTo(targetValue = 1f, animationSpec = FADE_TIME)
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
richTextState.config.unorderedListIndent = 4
|
||||||
|
richTextState.config.orderedListIndent = 20
|
||||||
|
}
|
||||||
|
LaunchedEffect(primaryColor) {
|
||||||
|
richTextState.config.linkColor = primaryColor
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionContainer {
|
||||||
|
RichText(
|
||||||
|
modifier = modifier
|
||||||
|
// Only animate size if the notes changes
|
||||||
|
.then(if (contentUpdatedOnce) Modifier.animateContentSize() else Modifier)
|
||||||
|
.alpha(alpha.value),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
state = richTextState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.EditNote
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.Button
|
||||||
|
import tachiyomi.presentation.core.components.material.ButtonDefaults
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaNotesSection(
|
||||||
|
content: String,
|
||||||
|
expanded: Boolean,
|
||||||
|
onEditNotes: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (content.isBlank()) return
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
MangaNotesDisplay(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
if (expanded) {
|
||||||
|
Button(
|
||||||
|
onClick = onEditNotes,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.EditNote,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(MR.strings.action_edit_notes),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = if (expanded) 0.dp else 12.dp,
|
||||||
|
bottom = if (expanded) 16.dp else 12.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewLightDark
|
||||||
|
@Composable
|
||||||
|
private fun MangaNotesSectionPreview() {
|
||||||
|
MangaNotesSection(
|
||||||
|
onEditNotes = {},
|
||||||
|
expanded = true,
|
||||||
|
content = "# Hello world\ntest1234 hi there!",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||||
|
import androidx.compose.material.icons.outlined.FormatBold
|
||||||
|
import androidx.compose.material.icons.outlined.FormatItalic
|
||||||
|
import androidx.compose.material.icons.outlined.FormatListNumbered
|
||||||
|
import androidx.compose.material.icons.outlined.FormatUnderlined
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||||
|
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
|
||||||
|
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
private const val MAX_LENGTH = 250
|
||||||
|
private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaNotesTextArea(
|
||||||
|
state: MangaNotesScreen.State,
|
||||||
|
onUpdate: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val richTextState = rememberRichTextState()
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
DisposableEffect(scope, richTextState) {
|
||||||
|
snapshotFlow { richTextState.annotatedString }
|
||||||
|
.debounce(0.25.seconds)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map { richTextState.toMarkdown() }
|
||||||
|
.onEach { onUpdate(it) }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
onUpdate(richTextState.toMarkdown())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
richTextState.setMarkdown(state.notes)
|
||||||
|
richTextState.config.unorderedListIndent = 4
|
||||||
|
richTextState.config.orderedListIndent = 20
|
||||||
|
}
|
||||||
|
LaunchedEffect(primaryColor) {
|
||||||
|
richTextState.config.linkColor = primaryColor
|
||||||
|
}
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
val textLength = remember(richTextState.annotatedString) { richTextState.toText().length }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
RichTextEditor(
|
||||||
|
state = richTextState,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLength = MAX_LENGTH,
|
||||||
|
placeholder = {
|
||||||
|
Text(text = stringResource(MR.strings.notes_placeholder))
|
||||||
|
},
|
||||||
|
colors = richTextEditorColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = MaterialTheme.padding.small)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
MangaNotesTextAreaButton(
|
||||||
|
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) },
|
||||||
|
isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold,
|
||||||
|
icon = Icons.Outlined.FormatBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MangaNotesTextAreaButton(
|
||||||
|
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) },
|
||||||
|
isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic,
|
||||||
|
icon = Icons.Outlined.FormatItalic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MangaNotesTextAreaButton(
|
||||||
|
onClick = {
|
||||||
|
richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||||
|
},
|
||||||
|
isSelected = richTextState.currentSpanStyle.textDecoration
|
||||||
|
?.contains(TextDecoration.Underline)
|
||||||
|
?: false,
|
||||||
|
icon = Icons.Outlined.FormatUnderlined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.extraSmall)
|
||||||
|
.height(MaterialTheme.padding.large),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MangaNotesTextAreaButton(
|
||||||
|
onClick = { richTextState.toggleUnorderedList() },
|
||||||
|
isSelected = richTextState.isUnorderedList,
|
||||||
|
icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MangaNotesTextAreaButton(
|
||||||
|
onClick = { richTextState.toggleOrderedList() },
|
||||||
|
isSelected = richTextState.isOrderedList,
|
||||||
|
icon = Icons.Outlined.FormatListNumbered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = (MAX_LENGTH - textLength).toString(),
|
||||||
|
color = if (textLength > MAX_LENGTH_WARN) {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(MaterialTheme.padding.extraSmall),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaNotesTextAreaButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: ImageVector,
|
||||||
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = true,
|
||||||
|
role = Role.Button,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent)
|
||||||
|
.padding(MaterialTheme.padding.extraSmall),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ fun MangaToolbar(
|
|||||||
onClickEditCategory: (() -> Unit)?,
|
onClickEditCategory: (() -> Unit)?,
|
||||||
onClickRefresh: () -> Unit,
|
onClickRefresh: () -> Unit,
|
||||||
onClickMigrate: (() -> Unit)?,
|
onClickMigrate: (() -> Unit)?,
|
||||||
|
onClickEditNotes: () -> Unit,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickEditInfo: (() -> Unit)?,
|
onClickEditInfo: (() -> Unit)?,
|
||||||
onClickRecommend: (() -> Unit)?,
|
onClickRecommend: (() -> Unit)?,
|
||||||
@@ -147,6 +148,12 @@ fun MangaToolbar(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_notes),
|
||||||
|
onClick = onClickEditNotes,
|
||||||
|
),
|
||||||
|
)
|
||||||
// SY -->
|
// SY -->
|
||||||
if (onClickMerge != null) {
|
if (onClickMerge != null) {
|
||||||
add(
|
add(
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.FirstBaseline
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
|
||||||
|
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||||
|
import com.mikepenz.markdown.compose.Markdown
|
||||||
|
import com.mikepenz.markdown.compose.components.markdownComponents
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownDivider
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTable
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownText
|
||||||
|
import com.mikepenz.markdown.compose.elements.listDepth
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
||||||
|
import com.mikepenz.markdown.model.MarkdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.MarkdownColors
|
||||||
|
import com.mikepenz.markdown.model.MarkdownPadding
|
||||||
|
import com.mikepenz.markdown.model.MarkdownTypography
|
||||||
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
|
import com.mikepenz.markdown.model.rememberMarkdownState
|
||||||
|
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
|
||||||
|
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
|
||||||
|
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||||
|
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
|
||||||
|
import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider
|
||||||
|
import org.intellij.markdown.parser.MarkerProcessor
|
||||||
|
import org.intellij.markdown.parser.MarkerProcessorFactory
|
||||||
|
import org.intellij.markdown.parser.ProductionHolder
|
||||||
|
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
|
||||||
|
import org.intellij.markdown.parser.constraints.MarkdownConstraints
|
||||||
|
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
|
||||||
|
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MarkdownRender(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
|
||||||
|
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
|
||||||
|
) {
|
||||||
|
Markdown(
|
||||||
|
markdownState = rememberMarkdownState(
|
||||||
|
content = content,
|
||||||
|
flavour = flavour,
|
||||||
|
immediate = true,
|
||||||
|
),
|
||||||
|
annotator = annotator,
|
||||||
|
colors = getMarkdownColors(),
|
||||||
|
typography = getMarkdownTypography(),
|
||||||
|
padding = markdownPadding,
|
||||||
|
components = markdownComponents,
|
||||||
|
imageTransformer = Coil3ImageTransformerImpl,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownColors(): MarkdownColors {
|
||||||
|
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
return DefaultMarkdownColors(
|
||||||
|
text = MaterialTheme.colorScheme.onSurface,
|
||||||
|
codeText = Color.Unspecified,
|
||||||
|
inlineCodeText = Color.Unspecified,
|
||||||
|
linkText = Color.Unspecified,
|
||||||
|
codeBackground = codeBackground,
|
||||||
|
inlineCodeBackground = codeBackground,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
tableText = Color.Unspecified,
|
||||||
|
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownTypography(): MarkdownTypography {
|
||||||
|
val link = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
return DefaultMarkdownTypography(
|
||||||
|
h1 = MaterialTheme.typography.headlineMedium,
|
||||||
|
h2 = MaterialTheme.typography.headlineSmall,
|
||||||
|
h3 = MaterialTheme.typography.titleLarge,
|
||||||
|
h4 = MaterialTheme.typography.titleMedium,
|
||||||
|
h5 = MaterialTheme.typography.titleSmall,
|
||||||
|
h6 = MaterialTheme.typography.bodyLarge,
|
||||||
|
text = MaterialTheme.typography.bodyMedium,
|
||||||
|
code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||||
|
inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||||
|
quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
|
||||||
|
paragraph = MaterialTheme.typography.bodyMedium,
|
||||||
|
ordered = MaterialTheme.typography.bodyMedium,
|
||||||
|
bullet = MaterialTheme.typography.bodyMedium,
|
||||||
|
list = MaterialTheme.typography.bodyMedium,
|
||||||
|
link = link,
|
||||||
|
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
||||||
|
table = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val markdownPadding = object : MarkdownPadding {
|
||||||
|
override val block: Dp = 2.dp
|
||||||
|
override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp)
|
||||||
|
override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(
|
||||||
|
left = 4.dp,
|
||||||
|
top = 2.dp,
|
||||||
|
right = 4.dp,
|
||||||
|
bottom = 2.dp,
|
||||||
|
)
|
||||||
|
override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp)
|
||||||
|
override val codeBlock: PaddingValues = PaddingValues(8.dp)
|
||||||
|
override val list: Dp = 0.dp
|
||||||
|
override val listIndent: Dp = 8.dp
|
||||||
|
override val listItemBottom: Dp = 0.dp
|
||||||
|
override val listItemTop: Dp = 0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
private val markdownComponents = markdownComponents(
|
||||||
|
horizontalRule = {
|
||||||
|
MarkdownDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = MaterialTheme.padding.extraSmall)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
orderedList = { ol ->
|
||||||
|
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||||
|
MarkdownOrderedList(
|
||||||
|
content = ol.content,
|
||||||
|
node = ol.node,
|
||||||
|
style = ol.typography.ordered,
|
||||||
|
depth = ol.listDepth,
|
||||||
|
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unorderedList = { ul ->
|
||||||
|
val markers = listOf("•", "◦", "▸", "▹")
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
|
||||||
|
MarkdownBulletList(
|
||||||
|
content = ul.content,
|
||||||
|
node = ul.node,
|
||||||
|
style = ul.typography.bullet,
|
||||||
|
markerModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
listModifier = { Modifier.alignBy(FirstBaseline) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table = { t ->
|
||||||
|
MarkdownTable(
|
||||||
|
content = t.content,
|
||||||
|
node = t.node,
|
||||||
|
style = t.typography.text,
|
||||||
|
headerBlock = { content, header, tableWidth, style ->
|
||||||
|
MarkdownTableHeader(
|
||||||
|
content = content,
|
||||||
|
header = header,
|
||||||
|
tableWidth = tableWidth,
|
||||||
|
style = style,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
rowBlock = { content, header, tableWidth, style ->
|
||||||
|
MarkdownTableRow(
|
||||||
|
content = content,
|
||||||
|
header = header,
|
||||||
|
tableWidth = tableWidth,
|
||||||
|
style = style,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
custom = { type, model ->
|
||||||
|
if (type in DISALLOWED_MARKDOWN_TYPES) {
|
||||||
|
MarkdownText(
|
||||||
|
content = model.content.substring(model.node.startOffset, model.node.endOffset),
|
||||||
|
style = model.typography.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
|
||||||
|
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
private object SimpleMarkdownProcessFactory : MarkerProcessorFactory {
|
||||||
|
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> {
|
||||||
|
return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `CommonMarkFlavour`, but with html blocks and reference links removed and
|
||||||
|
* table support added
|
||||||
|
*/
|
||||||
|
private class SimpleMarkdownMarkerProcessor(
|
||||||
|
productionHolder: ProductionHolder,
|
||||||
|
constraints: MarkdownConstraints,
|
||||||
|
) : CommonMarkMarkerProcessor(productionHolder, constraints) {
|
||||||
|
private val markerBlockProviders = listOf(
|
||||||
|
CodeBlockProvider(),
|
||||||
|
HorizontalRuleProvider(),
|
||||||
|
CodeFenceProvider(),
|
||||||
|
SetextHeaderProvider(),
|
||||||
|
BlockQuoteProvider(),
|
||||||
|
ListMarkerProvider(),
|
||||||
|
AtxHeaderProvider(),
|
||||||
|
GitHubTableMarkerProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> {
|
||||||
|
return markerBlockProviders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.more
|
package eu.kanade.presentation.more
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -13,13 +14,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import eu.kanade.presentation.manga.components.MarkdownRender
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
|
||||||
import com.halilibo.richtext.ui.material3.RichText
|
|
||||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -42,17 +40,15 @@ fun NewUpdateScreen(
|
|||||||
rejectText = stringResource(MR.strings.action_not_now),
|
rejectText = stringResource(MR.strings.action_not_now),
|
||||||
onRejectClick = onRejectUpdate,
|
onRejectClick = onRejectUpdate,
|
||||||
) {
|
) {
|
||||||
RichText(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = MaterialTheme.padding.large),
|
.padding(vertical = MaterialTheme.padding.large),
|
||||||
style = RichTextStyle(
|
|
||||||
stringStyle = RichTextStringStyle(
|
|
||||||
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Markdown(content = changelogInfo)
|
MarkdownRender(
|
||||||
|
content = changelogInfo,
|
||||||
|
flavour = GFMFlavourDescriptor(),
|
||||||
|
)
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onOpenInBrowser,
|
onClick = onOpenInBrowser,
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
val isLastStep = currentStep == steps.lastIndex
|
val isLastStep = currentStep == steps.lastIndex
|
||||||
|
|
||||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
BackHandler(enabled = currentStep != 0) {
|
||||||
|
currentStep--
|
||||||
|
}
|
||||||
|
|
||||||
InfoScreen(
|
InfoScreen(
|
||||||
icon = Icons.Outlined.RocketLaunch,
|
icon = Icons.Outlined.RocketLaunch,
|
||||||
|
|||||||
+11
-2
@@ -88,6 +88,7 @@ import tachiyomi.core.common.util.system.ImageUtil
|
|||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetAllManga
|
import tachiyomi.domain.manga.interactor.GetAllManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
@@ -114,6 +115,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
|
|
||||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||||
|
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||||
|
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
@@ -154,7 +156,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
getBackgroundActivityGroup(),
|
getBackgroundActivityGroup(),
|
||||||
getDataGroup(),
|
getDataGroup(),
|
||||||
getNetworkGroup(networkPreferences = networkPreferences),
|
getNetworkGroup(networkPreferences = networkPreferences),
|
||||||
getLibraryGroup(),
|
getLibraryGroup(libraryPreferences = libraryPreferences),
|
||||||
getReaderGroup(basePreferences = basePreferences),
|
getReaderGroup(basePreferences = basePreferences),
|
||||||
getExtensionsGroup(basePreferences = basePreferences),
|
getExtensionsGroup(basePreferences = basePreferences),
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -322,7 +324,9 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
private fun getLibraryGroup(
|
||||||
|
libraryPreferences: LibraryPreferences,
|
||||||
|
): Preference.PreferenceGroup {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -350,6 +354,11 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = libraryPreferences.updateMangaTitles(),
|
||||||
|
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
||||||
|
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -30,8 +30,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.ContentType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.semantics.contentType
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -228,7 +231,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
label = { Text(text = stringResource(uNameStringRes)) },
|
label = { Text(text = stringResource(uNameStringRes)) },
|
||||||
@@ -239,7 +244,9 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
|
|
||||||
var hidePassword by remember { mutableStateOf(true) }
|
var hidePassword by remember { mutableStateOf(true) }
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.semantics { contentType = ContentType.Password },
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it },
|
onValueChange = { password = it },
|
||||||
label = { Text(text = stringResource(MR.strings.password)) },
|
label = { Text(text = stringResource(MR.strings.password)) },
|
||||||
@@ -288,7 +295,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val id = if (processing) MR.strings.loading else MR.strings.login
|
val id = if (processing) MR.strings.logging_in else MR.strings.login
|
||||||
Text(text = stringResource(id))
|
Text(text = stringResource(id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
|
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|||||||
+38
-28
@@ -1,8 +1,10 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.advanced
|
package eu.kanade.presentation.more.settings.screen.advanced
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -12,6 +14,7 @@ import androidx.compose.material.icons.outlined.SelectAll
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -42,16 +45,16 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchUI
|
import tachiyomi.core.common.util.lang.launchUI
|
||||||
|
import tachiyomi.core.common.util.lang.toLong
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.domain.source.model.SourceWithCount
|
import tachiyomi.domain.source.model.SourceWithCount
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
|
||||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
@@ -73,18 +76,45 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||||
is ClearDatabaseScreenModel.State.Ready -> {
|
is ClearDatabaseScreenModel.State.Ready -> {
|
||||||
if (s.showConfirmation) {
|
if (s.showConfirmation) {
|
||||||
// SY -->
|
|
||||||
var keepReadManga by remember { mutableStateOf(true) }
|
var keepReadManga by remember { mutableStateOf(true) }
|
||||||
// SY <--
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.are_you_sure))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.clear_database_text))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.clear_db_exclude_read),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = keepReadManga,
|
||||||
|
onCheckedChange = { keepReadManga = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!keepReadManga) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.clear_database_history_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onDismissRequest = model::hideConfirmation,
|
onDismissRequest = model::hideConfirmation,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchUI {
|
scope.launchUI {
|
||||||
// SY -->
|
|
||||||
model.removeMangaBySourceId(keepReadManga)
|
model.removeMangaBySourceId(keepReadManga)
|
||||||
// SY <--
|
|
||||||
model.clearSelection()
|
model.clearSelection()
|
||||||
model.hideConfirmation()
|
model.hideConfirmation()
|
||||||
context.toast(MR.strings.clear_database_completed)
|
context.toast(MR.strings.clear_database_completed)
|
||||||
@@ -99,20 +129,6 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
Text(text = stringResource(MR.strings.action_cancel))
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
|
||||||
// SY -->
|
|
||||||
Column {
|
|
||||||
// SY <--
|
|
||||||
Text(text = stringResource(MR.strings.clear_database_confirmation))
|
|
||||||
// SY -->
|
|
||||||
LabeledCheckbox(
|
|
||||||
label = stringResource(SYMR.strings.clear_db_exclude_read),
|
|
||||||
checked = keepReadManga,
|
|
||||||
onCheckedChange = { keepReadManga = it },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,15 +240,9 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeMangaBySourceId(/* SY --> */keepReadManga: Boolean /* SY <-- */) = withNonCancellableContext {
|
suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext {
|
||||||
val state = state.value as? State.Ready ?: return@withNonCancellableContext
|
val state = state.value as? State.Ready ?: return@withNonCancellableContext
|
||||||
// SY -->
|
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong())
|
||||||
if (keepReadManga) {
|
|
||||||
database.mangasQueries.deleteMangasNotInLibraryAndNotReadBySourceIds(state.selection)
|
|
||||||
} else {
|
|
||||||
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
database.historyQueries.removeResettedHistory()
|
database.historyQueries.removeResettedHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -86,7 +85,8 @@ internal fun BasePreferenceWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
|
@Composable
|
||||||
|
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
|
||||||
var highlightFlag by remember { mutableStateOf(false) }
|
var highlightFlag by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
@@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
|
|||||||
},
|
},
|
||||||
label = "highlight",
|
label = "highlight",
|
||||||
)
|
)
|
||||||
Modifier.background(color = highlight)
|
return this.background(color = highlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val TrailingWidgetBuffer = 16.dp
|
internal val TrailingWidgetBuffer = 16.dp
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ fun UpdateScreen(
|
|||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode) {
|
||||||
|
onSelectAll(false)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
|
|||||||
@@ -1,12 +1,46 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.SeekableTransitionState
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.rememberTransition
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
@@ -15,18 +49,28 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
|||||||
import cafe.adriel.voyager.core.stack.StackEvent
|
import cafe.adriel.voyager.core.stack.StackEvent
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||||
|
import eu.kanade.tachiyomi.util.view.getWindowRadius
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.dropWhile
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
import soup.compose.material.motion.animation.materialSharedAxisXIn
|
||||||
|
import soup.compose.material.motion.animation.materialSharedAxisXOut
|
||||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For invoking back press to the parent activity
|
* For invoking back press to the parent activity
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ComposeCompositionLocalUsage")
|
||||||
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
||||||
|
|
||||||
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
||||||
@@ -59,39 +103,278 @@ interface AssistContentScreen {
|
|||||||
fun onProvideAssistUrl(): String?
|
fun onProvideAssistUrl(): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalVoyagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultNavigatorScreenTransition(
|
fun DefaultNavigatorScreenTransition(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val slideDistance = rememberSlideDistance()
|
val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
|
||||||
|
mutableStateOf(emptySet())
|
||||||
|
}
|
||||||
|
val currentScreens = navigator.items
|
||||||
|
DisposableEffect(currentScreens) {
|
||||||
|
onDispose {
|
||||||
|
val newScreenKeys = navigator.items.map { it.key }
|
||||||
|
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val slideDistance = rememberSlideDistance(slideDistance = 30.dp)
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
transition = {
|
|
||||||
materialSharedAxisX(
|
|
||||||
forward = navigator.lastEvent != StackEvent.Pop,
|
|
||||||
slideDistance = slideDistance,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
enterTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = { screen ->
|
||||||
|
if (this.transition.targetState == this.transition.currentState) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val newScreens = navigator.items.map { it.key }
|
||||||
|
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
|
||||||
|
if (screensToDispose.isNotEmpty()) {
|
||||||
|
screensToDispose.forEach { navigator.dispose(it) }
|
||||||
|
navigator.clearEvent()
|
||||||
|
}
|
||||||
|
screenCandidatesToDispose.value = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.Content()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SwipeEdge {
|
||||||
|
Unknown,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class AnimationType {
|
||||||
|
Pop,
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScreenTransition(
|
fun ScreenTransition(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
|
||||||
|
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
|
||||||
|
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
|
||||||
|
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
|
||||||
|
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
|
||||||
|
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
|
||||||
content: ScreenTransitionContent = { it.Content() },
|
content: ScreenTransitionContent = { it.Content() },
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
val view = LocalView.current
|
||||||
targetState = navigator.lastItem,
|
val viewConfig = LocalViewConfiguration.current
|
||||||
transitionSpec = transition,
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = remember {
|
||||||
|
ScreenTransitionState(
|
||||||
|
navigator = navigator,
|
||||||
|
scope = scope,
|
||||||
|
flingAnimationSpec = flingAnimationSpec(),
|
||||||
|
windowCornerRadius = view.getWindowRadius().toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val transitionState = remember { SeekableTransitionState(navigator.lastItem) }
|
||||||
|
val transition = rememberTransition(transitionState = transitionState)
|
||||||
|
|
||||||
|
if (state.isPredictiveBack || state.isAnimating) {
|
||||||
|
LaunchedEffect(state.progress) {
|
||||||
|
if (!state.isPredictiveBack) return@LaunchedEffect
|
||||||
|
val previousEntry = navigator.items.getOrNull(navigator.size - 2)
|
||||||
|
if (previousEntry != null) {
|
||||||
|
transitionState.seekTo(fraction = state.progress, targetState = previousEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(navigator) {
|
||||||
|
snapshotFlow { navigator.lastItem }
|
||||||
|
.collect {
|
||||||
|
state.cancelCancelAnimation()
|
||||||
|
if (it != transitionState.currentState) {
|
||||||
|
transitionState.animateTo(it)
|
||||||
|
} else {
|
||||||
|
transitionState.snapTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PredictiveBackHandler(enabled = navigator.canPop) { backEvent ->
|
||||||
|
state.cancelCancelAnimation()
|
||||||
|
var startOffset: Offset? = null
|
||||||
|
backEvent
|
||||||
|
.dropWhile {
|
||||||
|
if (startOffset == null) startOffset = Offset(it.touchX, it.touchY)
|
||||||
|
if (state.isAnimating) return@dropWhile true
|
||||||
|
// Touch slop check
|
||||||
|
val diff = Offset(it.touchX, it.touchY) - startOffset!!
|
||||||
|
diff.x.absoluteValue < viewConfig.touchSlop && diff.y.absoluteValue < viewConfig.touchSlop
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
if (it == null) {
|
||||||
|
state.finish()
|
||||||
|
} else {
|
||||||
|
state.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect {
|
||||||
|
state.setPredictiveBackProgress(
|
||||||
|
progress = it.progress,
|
||||||
|
swipeEdge = when (it.swipeEdge) {
|
||||||
|
BackEventCompat.EDGE_LEFT -> SwipeEdge.Left
|
||||||
|
BackEventCompat.EDGE_RIGHT -> SwipeEdge.Right
|
||||||
|
else -> SwipeEdge.Unknown
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.AnimatedContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
label = "transition",
|
transitionSpec = {
|
||||||
) { screen ->
|
val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack
|
||||||
navigator.saveableState("transition", screen) {
|
ContentTransform(
|
||||||
content(screen)
|
targetContentEnter = if (pop) {
|
||||||
|
popEnterTransition(state.swipeEdge)
|
||||||
|
} else {
|
||||||
|
enterTransition(state.swipeEdge)
|
||||||
|
},
|
||||||
|
initialContentExit = if (pop) {
|
||||||
|
popExitTransition(state.swipeEdge)
|
||||||
|
} else {
|
||||||
|
exitTransition(state.swipeEdge)
|
||||||
|
},
|
||||||
|
targetContentZIndex = if (pop) 0f else 1f,
|
||||||
|
sizeTransform = sizeTransform?.invoke(this),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentKey = { it.key },
|
||||||
|
) {
|
||||||
|
navigator.saveableState("transition", it) {
|
||||||
|
content(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
private class ScreenTransitionState(
|
||||||
|
private val navigator: Navigator,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val flingAnimationSpec: AnimationSpec<Float>,
|
||||||
|
windowCornerRadius: Float,
|
||||||
|
) {
|
||||||
|
var isPredictiveBack: Boolean by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var progress: Float by mutableFloatStateOf(0f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var swipeEdge: SwipeEdge by mutableStateOf(SwipeEdge.Unknown)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var animationJob: Pair<Job, AnimationType>? by mutableStateOf(null)
|
||||||
|
|
||||||
|
val isAnimating: Boolean
|
||||||
|
get() = animationJob?.first?.isActive == true
|
||||||
|
|
||||||
|
val windowCornerShape = RoundedCornerShape(windowCornerRadius)
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
this.isPredictiveBack = false
|
||||||
|
this.swipeEdge = SwipeEdge.Unknown
|
||||||
|
this.animationJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPredictiveBackProgress(progress: Float, swipeEdge: SwipeEdge) {
|
||||||
|
this.progress = lerp(0f, 0.65f, PredictiveBack.transform(progress))
|
||||||
|
this.swipeEdge = swipeEdge
|
||||||
|
this.isPredictiveBack = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish() {
|
||||||
|
if (!isPredictiveBack) {
|
||||||
|
navigator.pop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
animationJob = scope.launch {
|
||||||
|
try {
|
||||||
|
animate(
|
||||||
|
initialValue = progress,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = flingAnimationSpec,
|
||||||
|
block = { i, _ -> progress = i },
|
||||||
|
)
|
||||||
|
navigator.pop()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancelled
|
||||||
|
progress = 0f
|
||||||
|
} finally {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
} to AnimationType.Pop
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
if (!isPredictiveBack) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
animationJob = scope.launch {
|
||||||
|
try {
|
||||||
|
animate(
|
||||||
|
initialValue = progress,
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = flingAnimationSpec,
|
||||||
|
block = { i, _ -> progress = i },
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancelled
|
||||||
|
progress = 1f
|
||||||
|
} finally {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
} to AnimationType.Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelCancelAnimation() {
|
||||||
|
if (animationJob?.second == AnimationType.Cancel) {
|
||||||
|
animationJob?.first?.cancel()
|
||||||
|
animationJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<Screen>>, List<Screen>> {
|
||||||
|
return Saver(
|
||||||
|
save = { it.value.toList() },
|
||||||
|
restore = { mutableStateOf(it.toSet()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.presentation.webview
|
|||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -27,7 +26,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.kevinnzou.web.AccompanistWebViewClient
|
import com.kevinnzou.web.AccompanistWebViewClient
|
||||||
@@ -39,19 +37,13 @@ import eu.kanade.presentation.components.AppBar
|
|||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.getHtml
|
import eu.kanade.tachiyomi.util.system.getHtml
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Request
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebViewScreenContent(
|
fun WebViewScreenContent(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
@@ -65,11 +57,8 @@ fun WebViewScreenContent(
|
|||||||
) {
|
) {
|
||||||
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
||||||
val navigator = rememberWebViewNavigator()
|
val navigator = rememberWebViewNavigator()
|
||||||
val context = LocalContext.current
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val network = remember { Injekt.get<NetworkHelper>() }
|
|
||||||
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
|
|
||||||
|
|
||||||
var currentUrl by remember { mutableStateOf(url) }
|
var currentUrl by remember { mutableStateOf(url) }
|
||||||
var showCloudflareHelp by remember { mutableStateOf(false) }
|
var showCloudflareHelp by remember { mutableStateOf(false) }
|
||||||
@@ -124,40 +113,6 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
return super.shouldOverrideUrlLoading(view, request)
|
return super.shouldOverrideUrlLoading(view, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView?,
|
|
||||||
request: WebResourceRequest?,
|
|
||||||
): WebResourceResponse? {
|
|
||||||
return try {
|
|
||||||
val internalRequest = Request.Builder().apply {
|
|
||||||
url(request!!.url.toString())
|
|
||||||
request.requestHeaders.forEach { (key, value) ->
|
|
||||||
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
addHeader(key, value)
|
|
||||||
}
|
|
||||||
method(request.method, null)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
|
|
||||||
|
|
||||||
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
|
|
||||||
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
|
|
||||||
|
|
||||||
WebResourceResponse(
|
|
||||||
contentType,
|
|
||||||
contentEncoding,
|
|
||||||
response.code,
|
|
||||||
response.message,
|
|
||||||
response.headers.associate { it.first to it.second },
|
|
||||||
response.body.byteStream(),
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.stringResource(MR.strings.action_share),
|
context.stringResource(MR.strings.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
|
NotificationReceiver.shareBackupPendingActivity(context, file.uri),
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
|||||||
+1
@@ -135,6 +135,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
|
|||||||
lastModifiedAt = this.lastModifiedAt,
|
lastModifiedAt = this.lastModifiedAt,
|
||||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||||
version = this.version,
|
version = this.version,
|
||||||
|
notes = this.notes,
|
||||||
// SY -->
|
// SY -->
|
||||||
).also { backupManga ->
|
).also { backupManga ->
|
||||||
customMangaInfo?.let {
|
customMangaInfo?.let {
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ data class BackupManga(
|
|||||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||||
|
// Mihon values start here
|
||||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||||
@ProtoNumber(109) var version: Long = 0,
|
@ProtoNumber(109) var version: Long = 0,
|
||||||
|
@ProtoNumber(110) var notes: String = "",
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@@ -77,6 +79,7 @@ data class BackupManga(
|
|||||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||||
version = this@BackupManga.version,
|
version = this@BackupManga.version,
|
||||||
|
notes = this@BackupManga.notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ class MangaRestorer(
|
|||||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||||
version = manga.version,
|
version = manga.version,
|
||||||
isSyncing = 1,
|
isSyncing = 1,
|
||||||
|
notes = manga.notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return manga
|
return manga
|
||||||
@@ -292,6 +293,7 @@ class MangaRestorer(
|
|||||||
dateAdded = manga.dateAdded,
|
dateAdded = manga.dateAdded,
|
||||||
updateStrategy = manga.updateStrategy,
|
updateStrategy = manga.updateStrategy,
|
||||||
version = manga.version,
|
version = manga.version,
|
||||||
|
notes = manga.notes,
|
||||||
)
|
)
|
||||||
mangasQueries.selectLastInsertedRowId()
|
mangasQueries.selectLastInsertedRowId()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,41 @@ class DownloadCache(
|
|||||||
notifyChanges()
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a manga in this cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga being renamed.
|
||||||
|
* @param mangaUniFile the manga's new directory.
|
||||||
|
* @param newTitle the manga's new title.
|
||||||
|
*/
|
||||||
|
suspend fun renameManga(manga: Manga, mangaUniFile: UniFile, newTitle: String) {
|
||||||
|
rootDownloadsDirMutex.withLock {
|
||||||
|
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||||
|
val oldMangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
|
||||||
|
var oldChapterDirs: MutableSet<String>? = null
|
||||||
|
// Save the old name's cached chapter dirs
|
||||||
|
if (sourceDir.mangaDirs.containsKey(oldMangaDirName)) {
|
||||||
|
oldChapterDirs = sourceDir.mangaDirs[oldMangaDirName]?.chapterDirs
|
||||||
|
sourceDir.mangaDirs -= oldMangaDirName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve/create the cached manga directory for new name
|
||||||
|
val newMangaDirName = provider.getMangaDirName(newTitle)
|
||||||
|
var mangaDir = sourceDir.mangaDirs[newMangaDirName]
|
||||||
|
if (mangaDir == null) {
|
||||||
|
mangaDir = MangaDirectory(mangaUniFile)
|
||||||
|
sourceDir.mangaDirs += newMangaDirName to mangaDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the old chapters to new name's cache
|
||||||
|
if (!oldChapterDirs.isNullOrEmpty()) {
|
||||||
|
mangaDir.chapterDirs += oldChapterDirs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun removeSource(source: Source) {
|
suspend fun removeSource(source: Source) {
|
||||||
rootDownloadsDirMutex.withLock {
|
rootDownloadsDirMutex.withLock {
|
||||||
rootDownloadsDir.sourceDirs -= source.id
|
rootDownloadsDir.sourceDirs -= source.id
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class DownloadManager(
|
|||||||
|
|
||||||
return files.sortedBy { it.name }
|
return files.sortedBy { it.name }
|
||||||
.mapIndexed { i, file ->
|
.mapIndexed { i, file ->
|
||||||
Page(i, uri = file.uri).apply { status = Page.State.READY }
|
Page(i, uri = file.uri).apply { status = Page.State.Ready }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,10 +316,13 @@ class DownloadManager(
|
|||||||
|
|
||||||
if (removeNonFavorite && !manga.favorite) {
|
if (removeNonFavorite && !manga.favorite) {
|
||||||
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
|
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
|
||||||
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
|
.getOrNull()
|
||||||
mangaFolder.delete()
|
if (mangaFolder != null) {
|
||||||
cache.removeManga(manga)
|
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
|
||||||
return cleaned
|
mangaFolder.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||||
@@ -336,8 +339,8 @@ class DownloadManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cache.getDownloadCount(manga) == 0) {
|
if (cache.getDownloadCount(manga) == 0) {
|
||||||
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
|
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrNull()
|
||||||
if (!mangaFolder.listFiles().isNullOrEmpty()) {
|
if (mangaFolder != null && !mangaFolder.listFiles().isNullOrEmpty()) {
|
||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
} else {
|
} else {
|
||||||
@@ -395,6 +398,38 @@ class DownloadManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames manga download folder
|
||||||
|
*
|
||||||
|
* @param manga the manga
|
||||||
|
* @param newTitle the new manga title.
|
||||||
|
*/
|
||||||
|
suspend fun renameManga(manga: Manga, newTitle: String) {
|
||||||
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
val oldFolder = provider.findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return
|
||||||
|
val newName = provider.getMangaDirName(newTitle)
|
||||||
|
|
||||||
|
if (oldFolder.name == newName) return
|
||||||
|
|
||||||
|
// just to be safe, don't allow downloads for this manga while renaming it
|
||||||
|
downloader.removeFromQueue(manga)
|
||||||
|
|
||||||
|
val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
|
||||||
|
if (capitalizationChanged) {
|
||||||
|
val tempName = newName + Downloader.TMP_DIR_SUFFIX
|
||||||
|
if (!oldFolder.renameTo(tempName)) {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFolder.renameTo(newName)) {
|
||||||
|
cache.renameManga(manga, oldFolder, newTitle)
|
||||||
|
} else {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames an already downloaded chapter
|
* Renames an already downloaded chapter
|
||||||
*
|
*
|
||||||
@@ -405,7 +440,10 @@ class DownloadManager(
|
|||||||
*/
|
*/
|
||||||
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
|
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
|
||||||
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
|
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
|
||||||
|
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldDownload = oldNames.asSequence()
|
val oldDownload = oldNames.asSequence()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import tachiyomi.domain.storage.service.StorageManager
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to provide the directories where the downloads should be saved.
|
* This class is used to provide the directories where the downloads should be saved.
|
||||||
@@ -35,20 +36,36 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
|
internal fun getMangaDir(mangaTitle: String, source: Source): Result<UniFile> {
|
||||||
try {
|
val downloadsDir = downloadsDir
|
||||||
return downloadsDir!!
|
if (downloadsDir == null) {
|
||||||
.createDirectory(getSourceDirName(source))!!
|
logcat(LogPriority.ERROR) { "Failed to create download directory" }
|
||||||
.createDirectory(getMangaDirName(mangaTitle))!!
|
return Result.failure(
|
||||||
} catch (e: Throwable) {
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_download_directory)),
|
||||||
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
|
||||||
throw Exception(
|
|
||||||
context.stringResource(
|
|
||||||
MR.strings.invalid_location,
|
|
||||||
downloadsDir?.displayablePath ?: "",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sourceDirName = getSourceDirName(source)
|
||||||
|
val sourceDir = downloadsDir.createDirectory(sourceDirName)
|
||||||
|
if (sourceDir == null) {
|
||||||
|
val displayablePath = downloadsDir.displayablePath + "/$sourceDirName"
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to create source download directory: $displayablePath" }
|
||||||
|
return Result.failure(
|
||||||
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaDirName = getMangaDirName(mangaTitle)
|
||||||
|
val mangaDir = sourceDir.createDirectory(mangaDirName)
|
||||||
|
if (mangaDir == null) {
|
||||||
|
val displayablePath = sourceDir.displayablePath + "/$mangaDirName"
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to create manga download directory: $displayablePath" }
|
||||||
|
return Result.failure(
|
||||||
|
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success(mangaDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -327,7 +327,11 @@ class Downloader(
|
|||||||
* @param download the chapter to be downloaded.
|
* @param download the chapter to be downloaded.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadChapter(download: Download) {
|
private suspend fun downloadChapter(download: Download) {
|
||||||
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source)
|
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e ->
|
||||||
|
download.status = Download.State.ERROR
|
||||||
|
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
@@ -379,11 +383,11 @@ class Downloader(
|
|||||||
flow {
|
flow {
|
||||||
// Fetch image URL if necessary
|
// Fetch image URL if necessary
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LoadPage
|
||||||
try {
|
try {
|
||||||
page.imageUrl = download.source.getImageUrl(page)
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,12 +473,12 @@ class Downloader(
|
|||||||
|
|
||||||
page.uri = file.uri
|
page.uri = file.uri
|
||||||
page.progress = 100
|
page.progress = 100
|
||||||
page.status = Page.State.READY
|
page.status = Page.State.Ready
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
// Mark this page as error and allow to download the remaining
|
// Mark this page as error and allow to download the remaining
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.Error(e)
|
||||||
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,7 +498,7 @@ class Downloader(
|
|||||||
filename: String,
|
filename: String,
|
||||||
dataSaver: DataSaver,
|
dataSaver: DataSaver,
|
||||||
): UniFile {
|
): UniFile {
|
||||||
page.status = Page.State.DOWNLOAD_IMAGE
|
page.status = Page.State.DownloadImage
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
return flow {
|
return flow {
|
||||||
val response = source.getImage(page, dataSaver)
|
val response = source.getImage(page, dataSaver)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ data class Download(
|
|||||||
get() = pages?.sumOf(Page::progress) ?: 0
|
get() = pages?.sumOf(Page::progress) ?: 0
|
||||||
|
|
||||||
val downloadedImages: Int
|
val downloadedImages: Int
|
||||||
get() = pages?.count { it.status == Page.State.READY } ?: 0
|
get() = pages?.count { it.status == Page.State.Ready } ?: 0
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
|
||||||
|
|||||||
@@ -101,9 +101,12 @@ import java.time.Instant
|
|||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.incrementAndFetch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
@@ -343,7 +346,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
*/
|
*/
|
||||||
private suspend fun updateChapterList() {
|
private suspend fun updateChapterList() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInt(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
@@ -408,7 +411,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
downloadChapters(manga, chaptersToDownload)
|
downloadChapters(manga, chaptersToDownload)
|
||||||
hasDownloads.set(true)
|
hasDownloads.store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
|
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
|
||||||
@@ -441,7 +444,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads.get()) {
|
if (hasDownloads.load()) {
|
||||||
downloadManager.startDownloads()
|
downloadManager.startDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,7 +510,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInt(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -582,7 +585,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
var dbManga = getManga.await(networkManga.url, mangaDex.id)
|
var dbManga = getManga.await(networkManga.url, mangaDex.id)
|
||||||
|
|
||||||
if (dbManga == null) {
|
if (dbManga == null) {
|
||||||
dbManga = networkToLocalManga.await(
|
dbManga = networkToLocalManga(
|
||||||
Manga.create().copy(
|
Manga.create().copy(
|
||||||
url = networkManga.url,
|
url = networkManga.url,
|
||||||
ogTitle = networkManga.title,
|
ogTitle = networkManga.title,
|
||||||
@@ -641,7 +644,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
private suspend fun withUpdateNotification(
|
private suspend fun withUpdateNotification(
|
||||||
updatingManga: CopyOnWriteArrayList<Manga>,
|
updatingManga: CopyOnWriteArrayList<Manga>,
|
||||||
completed: AtomicInteger,
|
completed: AtomicInt,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
block: suspend () -> Unit,
|
block: suspend () -> Unit,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
@@ -650,7 +653,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
updatingManga.add(manga)
|
updatingManga.add(manga)
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -659,10 +662,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
updatingManga.remove(manga)
|
updatingManga.remove(manga)
|
||||||
completed.getAndIncrement()
|
completed.incrementAndFetch()
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ import tachiyomi.domain.source.service.SourceManager
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.fetchAndIncrement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
|
|
||||||
private suspend fun updateMetadata() {
|
private suspend fun updateMetadata() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInt(0)
|
||||||
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -142,7 +145,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
|
|
||||||
private suspend fun withUpdateNotification(
|
private suspend fun withUpdateNotification(
|
||||||
updatingManga: CopyOnWriteArrayList<Manga>,
|
updatingManga: CopyOnWriteArrayList<Manga>,
|
||||||
completed: AtomicInteger,
|
completed: AtomicInt,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
block: suspend () -> Unit,
|
block: suspend () -> Unit,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
@@ -151,7 +154,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
updatingManga.add(manga)
|
updatingManga.add(manga)
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,10 +163,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
updatingManga.remove(manga)
|
updatingManga.remove(manga)
|
||||||
completed.getAndIncrement()
|
completed.fetchAndIncrement()
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
updatingManga,
|
updatingManga,
|
||||||
completed.get(),
|
completed.load(),
|
||||||
mangaToUpdate.size,
|
mangaToUpdate.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -602,18 +602,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a share activity for a backup file.
|
* Returns [PendingIntent] that directly launches a share activity for a backup file.
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri uri of backup file
|
* @param uri uri of backup file
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent {
|
internal fun shareBackupPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = uri.toShareIntent(context, "application/x-protobuf+gzip").apply {
|
||||||
action = ACTION_SHARE_BACKUP
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
putExtra(EXTRA_URI, uri)
|
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class BangumiApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<BGMSearchResult>()
|
.parseAs<BGMSearchResult>()
|
||||||
.data
|
.data
|
||||||
|
.filter { it.platform == null || it.platform == "漫画" }
|
||||||
.map { it.toTrackSearch(trackId) }
|
.map { it.toTrackSearch(trackId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ data class BGMSubject(
|
|||||||
val volumes: Long = 0,
|
val volumes: Long = 0,
|
||||||
val eps: Long = 0,
|
val eps: Long = 0,
|
||||||
val rating: BGMSubjectRating?,
|
val rating: BGMSubjectRating?,
|
||||||
|
val platform: String?,
|
||||||
// SY -->
|
// SY -->
|
||||||
val infobox: List<Infobox> = emptyList(),
|
val infobox: List<Infobox> = emptyList(),
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
|||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
abstract class Installer(private val service: Service) {
|
abstract class Installer(private val service: Service) {
|
||||||
|
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
private var waitingInstall = AtomicReference<Entry>(null)
|
private var waitingInstall = AtomicReference<Entry?>(null)
|
||||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||||
|
|
||||||
private val cancelReceiver = object : BroadcastReceiver() {
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
@@ -79,7 +81,7 @@ abstract class Installer(private val service: Service) {
|
|||||||
* @see waitingInstall
|
* @see waitingInstall
|
||||||
*/
|
*/
|
||||||
fun continueQueue(resultStep: InstallStep) {
|
fun continueQueue(resultStep: InstallStep) {
|
||||||
val completedEntry = waitingInstall.getAndSet(null)
|
val completedEntry = waitingInstall.exchange(null)
|
||||||
if (completedEntry != null) {
|
if (completedEntry != null) {
|
||||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
checkQueue()
|
checkQueue()
|
||||||
@@ -115,10 +117,10 @@ abstract class Installer(private val service: Service) {
|
|||||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||||
queue.clear()
|
queue.clear()
|
||||||
waitingInstall.set(null)
|
waitingInstall.store(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
protected fun getActiveEntry(): Entry? = waitingInstall.load()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels queue for the provided download ID if exists.
|
* Cancels queue for the provided download ID if exists.
|
||||||
@@ -126,13 +128,13 @@ abstract class Installer(private val service: Service) {
|
|||||||
* @param downloadId Download ID as known by [ExtensionManager]
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
*/
|
*/
|
||||||
private fun cancelQueue(downloadId: Long) {
|
private fun cancelQueue(downloadId: Long) {
|
||||||
val waitingInstall = this.waitingInstall.get()
|
val waitingInstall = this.waitingInstall.load()
|
||||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||||
if (cancelEntry(toCancel)) {
|
if (cancelEntry(toCancel)) {
|
||||||
queue.remove(toCancel)
|
queue.remove(toCancel)
|
||||||
if (waitingInstall == toCancel) {
|
if (waitingInstall == toCancel) {
|
||||||
// Currently processing removed entry, continue queue
|
// Currently processing removed entry, continue queue
|
||||||
this.waitingInstall.set(null)
|
this.waitingInstall.store(null)
|
||||||
checkQueue()
|
checkQueue()
|
||||||
}
|
}
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class MergedSource : HttpSource() {
|
|||||||
var manga = getManga.await(mangaUrl, mangaSourceId)
|
var manga = getManga.await(mangaUrl, mangaSourceId)
|
||||||
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
|
||||||
if (manga == null) {
|
if (manga == null) {
|
||||||
val newManga = networkToLocalManga.await(
|
val newManga = networkToLocalManga(
|
||||||
Manga.create().copy(
|
Manga.create().copy(
|
||||||
source = mangaSourceId,
|
source = mangaSourceId,
|
||||||
url = mangaUrl,
|
url = mangaUrl,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.ui.util.fastAny
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.browse.FeedItemUI
|
import eu.kanade.presentation.browse.FeedItemUI
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
@@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -251,9 +251,7 @@ open class FeedScreenModel(
|
|||||||
|
|
||||||
val result = withIOContext {
|
val result = withIOContext {
|
||||||
itemUI.copy(
|
itemUI.copy(
|
||||||
results = page.map {
|
results = networkToLocalManga(page.map { it.toDomainManga(itemUI.source!!.id) }),
|
||||||
networkToLocalManga.await(it.toDomainManga(itemUI.source!!.id))
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ object MigrationFlags {
|
|||||||
const val CUSTOM_COVER = 0b01000
|
const val CUSTOM_COVER = 0b01000
|
||||||
const val EXTRA = 0b10000
|
const val EXTRA = 0b10000
|
||||||
const val DELETE_CHAPTERS = 0b100000
|
const val DELETE_CHAPTERS = 0b100000
|
||||||
|
const val NOTES = 0b1000000
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
@@ -32,4 +33,8 @@ object MigrationFlags {
|
|||||||
fun hasDeleteChapters(value: Int): Boolean {
|
fun hasDeleteChapters(value: Int): Boolean {
|
||||||
return value and DELETE_CHAPTERS != 0
|
return value and DELETE_CHAPTERS != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasNotes(value: Int): Boolean {
|
||||||
|
return value and NOTES != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -59,6 +59,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||||
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
|
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
|
||||||
|
binding.migNotes.isChecked = MigrationFlags.hasNotes(flags)
|
||||||
|
|
||||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
@@ -66,6 +67,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
|
binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
||||||
|
|
||||||
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
||||||
binding.extraSearchParamText.isVisible = false
|
binding.extraSearchParamText.isVisible = false
|
||||||
@@ -108,6 +110,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
|
|||||||
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
||||||
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
||||||
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
|
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
|
||||||
|
if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES
|
||||||
preferences.migrateFlags().set(flags)
|
preferences.migrateFlags().set(flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -226,7 +226,7 @@ class MigrationListScreenModel(
|
|||||||
if (searchResult != null &&
|
if (searchResult != null &&
|
||||||
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
|
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
|
||||||
) {
|
) {
|
||||||
val localManga = networkToLocalManga.await(searchResult)
|
val localManga = networkToLocalManga(searchResult)
|
||||||
|
|
||||||
val chapters = if (source is EHentai) {
|
val chapters = if (source is EHentai) {
|
||||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||||
@@ -264,7 +264,7 @@ class MigrationListScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchResult != null) {
|
if (searchResult != null) {
|
||||||
val localManga = networkToLocalManga.await(searchResult)
|
val localManga = networkToLocalManga(searchResult)
|
||||||
val chapters = try {
|
val chapters = try {
|
||||||
if (source is EHentai) {
|
if (source is EHentai) {
|
||||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||||
@@ -455,7 +455,7 @@ class MigrationListScreenModel(
|
|||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
val result = migratingManga.migrationScope.async {
|
val result = migratingManga.migrationScope.async {
|
||||||
val manga = getManga(newMangaId)!!
|
val manga = getManga(newMangaId)!!
|
||||||
val localManga = networkToLocalManga.await(manga)
|
val localManga = networkToLocalManga(manga)
|
||||||
try {
|
try {
|
||||||
val source = sourceManager.get(manga.source)!!
|
val source = sourceManager.get(manga.source)!!
|
||||||
val chapters = source.getChapterList(localManga.toSManga())
|
val chapters = source.getChapterList(localManga.toSManga())
|
||||||
|
|||||||
+12
-9
@@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
@@ -150,7 +151,11 @@ data class BrowseSourceScreen(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.pointerInput(Unit) {},
|
||||||
|
) {
|
||||||
BrowseSourceToolbar(
|
BrowseSourceToolbar(
|
||||||
searchQuery = state.toolbarQuery,
|
searchQuery = state.toolbarQuery,
|
||||||
onSearchQueryChange = screenModel::setToolbarQuery,
|
onSearchQueryChange = screenModel::setToolbarQuery,
|
||||||
@@ -256,14 +261,11 @@ data class BrowseSourceScreen(
|
|||||||
onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) },
|
onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) },
|
||||||
onMangaLongClick = { manga ->
|
onMangaLongClick = { manga ->
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
val duplicates = screenModel.getDuplicateLibraryManga(manga)
|
||||||
when {
|
when {
|
||||||
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
||||||
duplicateManga != null -> screenModel.setDialog(
|
duplicates.isNotEmpty() -> screenModel.setDialog(
|
||||||
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
|
||||||
manga,
|
|
||||||
duplicateManga,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else -> screenModel.addFavorite(manga)
|
else -> screenModel.addFavorite(manga)
|
||||||
}
|
}
|
||||||
@@ -318,15 +320,16 @@ data class BrowseSourceScreen(
|
|||||||
}
|
}
|
||||||
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
|
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||||
onMigrate = {
|
onMigrate = {
|
||||||
// SY -->
|
// SY -->
|
||||||
PreMigrationScreen.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
navigator,
|
navigator,
|
||||||
dialog.duplicate.id,
|
it.id,
|
||||||
dialog.manga.id,
|
dialog.manga.id,
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
+10
-15
@@ -16,7 +16,6 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
||||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@@ -50,7 +48,6 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import tachiyomi.core.common.preference.CheckboxState
|
import tachiyomi.core.common.preference.CheckboxState
|
||||||
@@ -67,15 +64,15 @@ import tachiyomi.domain.library.service.LibraryPreferences
|
|||||||
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.source.interactor.DeleteSavedSearchById
|
import tachiyomi.domain.source.interactor.DeleteSavedSearchById
|
||||||
import tachiyomi.domain.source.interactor.GetRemoteManga
|
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||||
import tachiyomi.domain.source.interactor.InsertSavedSearch
|
import tachiyomi.domain.source.interactor.InsertSavedSearch
|
||||||
import tachiyomi.domain.source.model.EXHSavedSearch
|
import tachiyomi.domain.source.model.EXHSavedSearch
|
||||||
import tachiyomi.domain.source.model.SavedSearch
|
import tachiyomi.domain.source.model.SavedSearch
|
||||||
import tachiyomi.domain.source.repository.SourcePagingSourceType
|
import tachiyomi.domain.source.repository.SourcePagingSource
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -101,7 +98,6 @@ open class BrowseSourceScreenModel(
|
|||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
|
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val addTracks: AddTracks = Injekt.get(),
|
private val addTracks: AddTracks = Injekt.get(),
|
||||||
private val getIncognitoState: GetIncognitoState = Injekt.get(),
|
private val getIncognitoState: GetIncognitoState = Injekt.get(),
|
||||||
@@ -193,10 +189,9 @@ open class BrowseSourceScreenModel(
|
|||||||
createSourcePagingSource(listing.query ?: "", listing.filters)
|
createSourcePagingSource(listing.query ?: "", listing.filters)
|
||||||
// SY <--
|
// SY <--
|
||||||
}.flow.map { pagingData ->
|
}.flow.map { pagingData ->
|
||||||
pagingData.map { (it, metadata) ->
|
pagingData.map { (manga, metadata) ->
|
||||||
networkToLocalManga.await(it.toDomainManga(sourceId))
|
getManga.subscribe(manga.url, manga.source)
|
||||||
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
|
.map { it ?: manga }
|
||||||
.filterNotNull()
|
|
||||||
// SY -->
|
// SY -->
|
||||||
.combineMetadata(metadata)
|
.combineMetadata(metadata)
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -382,8 +377,8 @@ open class BrowseSourceScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSource {
|
||||||
return getRemoteManga.subscribe(sourceId, query, filters)
|
return getRemoteManga(sourceId, query, filters)
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@@ -399,8 +394,8 @@ open class BrowseSourceScreenModel(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
|
suspend fun getDuplicateLibraryManga(manga: Manga): List<MangaWithChapterCount> {
|
||||||
return getDuplicateLibraryManga.await(manga).getOrNull(0)
|
return getDuplicateLibraryManga.invoke(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
|
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
|
||||||
@@ -450,7 +445,7 @@ open class BrowseSourceScreenModel(
|
|||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object Filter : Dialog
|
data object Filter : Dialog
|
||||||
data class RemoveManga(val manga: Manga) : Dialog
|
data class RemoveManga(val manga: Manga) : Dialog
|
||||||
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
||||||
data class ChangeMangaCategory(
|
data class ChangeMangaCategory(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||||
|
|||||||
+16
-14
@@ -188,22 +188,24 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit/* SY --> */, star
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
filter.values.mapIndexed { index, item ->
|
filter.values.mapIndexed { index, item ->
|
||||||
|
val sortAscending = filter.state?.ascending
|
||||||
|
?.takeIf { index == filter.state?.index }
|
||||||
SortItem(
|
SortItem(
|
||||||
label = item,
|
label = item,
|
||||||
sortDescending = filter.state?.ascending?.not()
|
sortDescending = if (sortAscending != null) !sortAscending else null,
|
||||||
?.takeIf { index == filter.state?.index },
|
onClick = {
|
||||||
) {
|
val ascending = if (index == filter.state?.index) {
|
||||||
val ascending = if (index == filter.state?.index) {
|
!filter.state!!.ascending
|
||||||
!filter.state!!.ascending
|
} else {
|
||||||
} else {
|
filter.state?.ascending ?: true
|
||||||
filter.state!!.ascending
|
}
|
||||||
}
|
filter.state = Filter.Sort.Selection(
|
||||||
filter.state = Filter.Sort.Selection(
|
index = index,
|
||||||
index = index,
|
ascending = ascending,
|
||||||
ascending = ascending,
|
)
|
||||||
)
|
onUpdate()
|
||||||
onUpdate()
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -10,7 +10,6 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.browse.SourceFeedUI
|
import eu.kanade.presentation.browse.SourceFeedUI
|
||||||
@@ -32,8 +31,8 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -173,9 +172,7 @@ open class SourceFeedScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val titles = withIOContext {
|
val titles = withIOContext {
|
||||||
page.map {
|
networkToLocalManga(page.map { it.toDomainManga(source.id) })
|
||||||
networkToLocalManga.await(it.toDomainManga(source.id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
|
|||||||
+5
-4
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.util.ioCoroutineScope
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
@@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.preference.toggle
|
import tachiyomi.core.common.preference.toggle
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
@@ -169,9 +169,10 @@ abstract class SearchScreenModel(
|
|||||||
source.getSearchManga(1, query, source.getFilterList())
|
source.getSearchManga(1, query, source.getFilterList())
|
||||||
}
|
}
|
||||||
|
|
||||||
val titles = page.mangas.map {
|
val titles = page.mangas
|
||||||
networkToLocalManga.await(it.toDomainManga(source.id))
|
.map { it.toDomainManga(source.id) }
|
||||||
}
|
.distinctBy { it.url }
|
||||||
|
.let { networkToLocalManga(it) }
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
updateItem(source, SearchItemResult.Success(titles))
|
updateItem(source, SearchItemResult.Success(titles))
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import androidx.compose.runtime.Immutable
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ResolvableSource
|
import eu.kanade.tachiyomi.source.online.ResolvableSource
|
||||||
import eu.kanade.tachiyomi.source.online.UriType
|
import eu.kanade.tachiyomi.source.online.UriType
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
@@ -27,7 +25,6 @@ class DeepLinkScreenModel(
|
|||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||||
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
|
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
|
||||||
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||||
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
|
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ class DeepLinkScreenModel(
|
|||||||
.firstOrNull { it.getUriType(query) != UriType.Unknown }
|
.firstOrNull { it.getUriType(query) != UriType.Unknown }
|
||||||
|
|
||||||
val manga = source?.getManga(query)?.let {
|
val manga = source?.getManga(query)?.let {
|
||||||
getMangaFromSManga(it, source.id)
|
networkToLocalManga(it.toDomainManga(source.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
|
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
|
||||||
@@ -73,11 +70,6 @@ class DeepLinkScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
|
|
||||||
return getMangaByUrlAndSourceId.await(sManga.url, sourceId)
|
|
||||||
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
@Immutable
|
@Immutable
|
||||||
data object Loading : State
|
data object Loading : State
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
|
|||||||
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -174,9 +175,9 @@ class HistoryScreenModel(
|
|||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
val manga = getManga.await(mangaId) ?: return@launchIO
|
val manga = getManga.await(mangaId) ?: return@launchIO
|
||||||
|
|
||||||
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0)
|
val duplicates = getDuplicateLibraryManga(manga)
|
||||||
if (duplicate != null) {
|
if (duplicates.isNotEmpty()) {
|
||||||
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) }
|
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +247,7 @@ class HistoryScreenModel(
|
|||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object DeleteAll : Dialog
|
data object DeleteAll : Dialog
|
||||||
data class Delete(val history: HistoryWithRelations) : Dialog
|
data class Delete(val history: HistoryWithRelations) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
||||||
data class ChangeCategory(
|
data class ChangeCategory(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
|
|||||||
@@ -114,17 +114,18 @@ data object HistoryTab : Tab {
|
|||||||
}
|
}
|
||||||
is HistoryScreenModel.Dialog.DuplicateManga -> {
|
is HistoryScreenModel.Dialog.DuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
screenModel.addFavorite(dialog.manga)
|
screenModel.addFavorite(dialog.manga)
|
||||||
},
|
},
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||||
onMigrate = {
|
onMigrate = {
|
||||||
// SY -->
|
// SY -->
|
||||||
PreMigrationScreen.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
navigator,
|
navigator,
|
||||||
dialog.duplicate.id,
|
it.id,
|
||||||
dialog.manga.id,
|
dialog.manga.id,
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -148,7 +149,7 @@ data object HistoryTab : Tab {
|
|||||||
screenModel = MigrateDialogScreenModel(),
|
screenModel = MigrateDialogScreenModel(),
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||||
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
|
onPopScreen = onDismissRequest,
|
||||||
)
|
)
|
||||||
} SY <--*/
|
} SY <--*/
|
||||||
null -> {}
|
null -> {}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.home
|
package eu.kanade.tachiyomi.ui.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationRailItem
|
import androidx.compose.material3.NavigationRailItem
|
||||||
@@ -23,15 +24,24 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastFilter
|
import androidx.compose.ui.util.fastFilter
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
@@ -53,6 +63,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughIn
|
import soup.compose.material.motion.animation.materialFadeThroughIn
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughOut
|
import soup.compose.material.motion.animation.materialFadeThroughOut
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
@@ -61,8 +72,10 @@ import tachiyomi.presentation.core.components.material.NavigationBar
|
|||||||
import tachiyomi.presentation.core.components.material.NavigationRail
|
import tachiyomi.presentation.core.components.material.NavigationRail
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
object HomeScreen : Screen() {
|
object HomeScreen : Screen() {
|
||||||
|
|
||||||
@@ -70,8 +83,14 @@ object HomeScreen : Screen() {
|
|||||||
private val openTabEvent = Channel<Tab>()
|
private val openTabEvent = Channel<Tab>()
|
||||||
private val showBottomNavEvent = Channel<Boolean>()
|
private val showBottomNavEvent = Channel<Boolean>()
|
||||||
|
|
||||||
private const val TAB_FADE_DURATION = 200
|
@Suppress("ConstPropertyName")
|
||||||
private const val TAB_NAVIGATOR_KEY = "HomeTabs"
|
private const val TabFadeDuration = 200
|
||||||
|
|
||||||
|
@Suppress("ConstPropertyName")
|
||||||
|
private const val TabNavigatorKey = "HomeTabs"
|
||||||
|
|
||||||
|
@SuppressLint("ComposeCompositionLocalUsage")
|
||||||
|
val LocalHomeScreenInsetsProvider = staticCompositionLocalOf { WindowInsets(0.dp) }
|
||||||
|
|
||||||
private val TABS = listOf(
|
private val TABS = listOf(
|
||||||
LibraryTab,
|
LibraryTab,
|
||||||
@@ -84,6 +103,7 @@ object HomeScreen : Screen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -94,7 +114,7 @@ object HomeScreen : Screen() {
|
|||||||
|
|
||||||
TabNavigator(
|
TabNavigator(
|
||||||
tab = LibraryTab,
|
tab = LibraryTab,
|
||||||
key = TAB_NAVIGATOR_KEY,
|
key = TabNavigatorKey,
|
||||||
) { tabNavigator ->
|
) { tabNavigator ->
|
||||||
// Provide usable navigator to content screen
|
// Provide usable navigator to content screen
|
||||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||||
@@ -134,26 +154,62 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentWindowInsets = WindowInsets(0),
|
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.graphicsLayer {
|
||||||
.consumeWindowInsets(contentPadding),
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
.windowInsetsPadding(
|
||||||
|
remember {
|
||||||
|
object : WindowInsets {
|
||||||
|
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int {
|
||||||
|
return with(density) {
|
||||||
|
contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int {
|
||||||
|
return with(density) {
|
||||||
|
contentPadding.calculateRightPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBottom(density: Density): Int = 0
|
||||||
|
|
||||||
|
override fun getTop(density: Density): Int = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
|
val insets = remember {
|
||||||
|
object : WindowInsets {
|
||||||
|
override fun getBottom(density: Density): Int {
|
||||||
|
return with(density) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTop(density: Density): Int {
|
||||||
|
return with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0
|
||||||
|
|
||||||
|
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = tabNavigator.current,
|
targetState = tabNavigator.current,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
materialFadeThroughIn(
|
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
|
||||||
initialScale = 1f,
|
materialFadeThroughOut(durationMillis = TabFadeDuration)
|
||||||
durationMillis = TAB_FADE_DURATION,
|
|
||||||
) togetherWith
|
|
||||||
materialFadeThroughOut(durationMillis = TAB_FADE_DURATION)
|
|
||||||
},
|
},
|
||||||
label = "tabContent",
|
label = "tabContent",
|
||||||
) {
|
) {
|
||||||
tabNavigator.saveableState(key = "currentTab", it) {
|
CompositionLocalProvider(LocalHomeScreenInsetsProvider provides insets) {
|
||||||
it.Content()
|
tabNavigator.saveableState(key = "currentTab", it) {
|
||||||
|
it.Content()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +217,32 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
||||||
BackHandler(
|
|
||||||
enabled = tabNavigator.current != LibraryTab,
|
var handlingBack by remember { mutableStateOf(false) }
|
||||||
onBack = goToLibraryTab,
|
PredictiveBackHandler(
|
||||||
)
|
enabled = handlingBack || tabNavigator.current::class != LibraryTab::class,
|
||||||
|
) { progress ->
|
||||||
|
handlingBack = true
|
||||||
|
val currentTab = tabNavigator.current
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.92f, PredictiveBack.transform(backEvent.progress))
|
||||||
|
tabNavigator.current = if (backEvent.progress > 0.25f) TABS[0] else currentTab
|
||||||
|
}
|
||||||
|
goToLibraryTab()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
tabNavigator.current = currentTab
|
||||||
|
} finally {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
handlingBack = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
launch {
|
||||||
@@ -312,8 +390,6 @@ object HomeScreen : Screen() {
|
|||||||
Icon(
|
Icon(
|
||||||
painter = tab.options.icon!!,
|
painter = tab.options.icon!!,
|
||||||
contentDescription = tab.options.title,
|
contentDescription = tab.options.title,
|
||||||
// TODO: https://issuetracker.google.com/u/0/issues/316327367
|
|
||||||
tint = LocalContentColor.current,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
|||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog
|
import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen
|
import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
||||||
@@ -205,6 +206,7 @@ class MangaScreen(
|
|||||||
successState.manga.favorite
|
successState.manga.favorite
|
||||||
},
|
},
|
||||||
previewsRowCount = successState.previewsRowCount,
|
previewsRowCount = successState.previewsRowCount,
|
||||||
|
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||||
// SY -->
|
// SY -->
|
||||||
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
||||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||||
@@ -259,12 +261,13 @@ class MangaScreen(
|
|||||||
|
|
||||||
is MangaScreenModel.Dialog.DuplicateManga -> {
|
is MangaScreenModel.Dialog.DuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||||
onMigrate = {
|
onMigrate = {
|
||||||
// SY -->
|
// SY -->
|
||||||
migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id)
|
migrateManga(navigator, it, screenModel.manga!!.id)
|
||||||
// SY <--
|
// SY <--
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ import tachiyomi.domain.manga.interactor.UpdateMergedSettings
|
|||||||
import tachiyomi.domain.manga.model.CustomMangaInfo
|
import tachiyomi.domain.manga.model.CustomMangaInfo
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
|
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
|
||||||
import tachiyomi.domain.manga.model.MergedMangaReference
|
import tachiyomi.domain.manga.model.MergedMangaReference
|
||||||
import tachiyomi.domain.manga.model.applyFilter
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
@@ -658,7 +659,7 @@ class MangaScreenModel(
|
|||||||
existingManga = getManga.await(mergedManga.url, mergedManga.source)
|
existingManga = getManga.await(mergedManga.url, mergedManga.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedManga = networkToLocalManga.await(mergedManga)
|
mergedManga = networkToLocalManga(mergedManga)
|
||||||
|
|
||||||
getCategories.await(originalMangaId)
|
getCategories.await(originalMangaId)
|
||||||
.let {
|
.let {
|
||||||
@@ -783,10 +784,10 @@ class MangaScreenModel(
|
|||||||
// Add to library
|
// Add to library
|
||||||
// First, check if duplicate exists if callback is provided
|
// First, check if duplicate exists if callback is provided
|
||||||
if (checkDuplicate) {
|
if (checkDuplicate) {
|
||||||
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0)
|
val duplicates = getDuplicateLibraryManga(manga)
|
||||||
|
|
||||||
if (duplicate != null) {
|
if (duplicates.isNotEmpty()) {
|
||||||
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) }
|
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1653,7 +1654,7 @@ class MangaScreenModel(
|
|||||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
) : Dialog
|
) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
||||||
|
|
||||||
/* SY -->
|
/* SY -->
|
||||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga.notes
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.manga.MangaNotesScreen
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
|
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MangaNotesScreen(
|
||||||
|
private val manga: Manga,
|
||||||
|
) : Screen() {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { Model(manga) }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
MangaNotesScreen(
|
||||||
|
state = state,
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
onUpdate = screenModel::updateNotes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Model(
|
||||||
|
private val manga: Manga,
|
||||||
|
private val updateMangaNotes: UpdateMangaNotes = Injekt.get(),
|
||||||
|
) : StateScreenModel<State>(State(manga, manga.notes)) {
|
||||||
|
|
||||||
|
fun updateNotes(content: String) {
|
||||||
|
if (content == state.value.notes) return
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(notes = content)
|
||||||
|
}
|
||||||
|
|
||||||
|
screenModelScope.launchNonCancellable {
|
||||||
|
updateMangaNotes(manga.id, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class State(
|
||||||
|
val manga: Manga,
|
||||||
|
val notes: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -33,12 +33,9 @@ class OnboardingScreen : Screen() {
|
|||||||
|
|
||||||
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
|
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
|
||||||
|
|
||||||
BackHandler(
|
BackHandler(enabled = !shownOnboardingFlow) {
|
||||||
enabled = !shownOnboardingFlow,
|
// Prevent exiting if onboarding hasn't been completed
|
||||||
onBack = {
|
}
|
||||||
// Prevent exiting if onboarding hasn't been completed
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
onComplete = finishOnboarding,
|
onComplete = finishOnboarding,
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
?.pages
|
?.pages
|
||||||
?.forEachIndexed { _, page ->
|
?.forEachIndexed { _, page ->
|
||||||
var shouldQueuePage = false
|
var shouldQueuePage = false
|
||||||
if (page.status == Page.State.ERROR) {
|
if (page.status is Page.State.Error) {
|
||||||
shouldQueuePage = true
|
shouldQueuePage = true
|
||||||
} /*else if (page.status == Page.LOAD_PAGE ||
|
} /*else if (page.status == Page.LOAD_PAGE ||
|
||||||
page.status == Page.DOWNLOAD_IMAGE) {
|
page.status == Page.DOWNLOAD_IMAGE) {
|
||||||
@@ -725,7 +725,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
if (shouldQueuePage) {
|
if (shouldQueuePage) {
|
||||||
page.status = Page.State.QUEUE
|
page.status = Page.State.Queue
|
||||||
} else {
|
} else {
|
||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
}
|
}
|
||||||
@@ -758,11 +758,11 @@ class ReaderActivity : BaseActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (curPage.status == Page.State.ERROR) {
|
if (curPage.status is Page.State.Error) {
|
||||||
toast(SYMR.strings.eh_boost_page_errored)
|
toast(SYMR.strings.eh_boost_page_errored)
|
||||||
} else if (curPage.status == Page.State.LOAD_PAGE || curPage.status == Page.State.DOWNLOAD_IMAGE) {
|
} else if (curPage.status == Page.State.LoadPage || curPage.status == Page.State.DownloadImage) {
|
||||||
toast(SYMR.strings.eh_boost_page_downloading)
|
toast(SYMR.strings.eh_boost_page_downloading)
|
||||||
} else if (curPage.status == Page.State.READY) {
|
} else if (curPage.status == Page.State.Ready) {
|
||||||
toast(SYMR.strings.eh_boost_page_downloaded)
|
toast(SYMR.strings.eh_boost_page_downloaded)
|
||||||
} else {
|
} else {
|
||||||
val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader)
|
val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import eu.kanade.domain.sync.SyncPreferences
|
|||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import eu.kanade.domain.track.service.TrackPreferences
|
import eu.kanade.domain.track.service.TrackPreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.isRecognizedNumber
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
@@ -184,6 +183,11 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var chapterToDownload: Download? = null
|
private var chapterToDownload: Download? = null
|
||||||
|
|
||||||
|
private val unfilteredChapterList by lazy {
|
||||||
|
val manga = manga!!
|
||||||
|
runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
||||||
* time in a background thread to avoid blocking the UI.
|
* time in a background thread to avoid blocking the UI.
|
||||||
@@ -693,7 +697,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
readerChapter.requestedPage = pageIndex
|
readerChapter.requestedPage = pageIndex
|
||||||
chapterPageIndex = pageIndex
|
chapterPageIndex = pageIndex
|
||||||
|
|
||||||
if (!incognitoMode && page.status != Page.State.ERROR) {
|
if (!incognitoMode && page.status !is Page.State.Error) {
|
||||||
readerChapter.chapter.last_page_read = pageIndex
|
readerChapter.chapter.last_page_read = pageIndex
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@@ -732,11 +736,11 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
// SY -->
|
// SY -->
|
||||||
if (manga?.isEhBasedManga() == true) {
|
if (manga?.isEhBasedManga() == true) {
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
val chapterUpdates = chapterList
|
val chapterUpdates = unfilteredChapterList
|
||||||
.filter { it.chapter.source_order > readerChapter.chapter.source_order }
|
.filter { it.sourceOrder > readerChapter.chapter.source_order }
|
||||||
.map { chapter ->
|
.map { chapter ->
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
id = chapter.chapter.id!!,
|
id = chapter.id,
|
||||||
read = true,
|
read = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -752,15 +756,14 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING)
|
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING)
|
||||||
if (!markDuplicateAsRead) return
|
if (!markDuplicateAsRead) return
|
||||||
|
|
||||||
val duplicateUnreadChapters = chapterList
|
val duplicateUnreadChapters = unfilteredChapterList
|
||||||
.mapNotNull {
|
.mapNotNull { chapter ->
|
||||||
val chapter = it.chapter
|
|
||||||
if (
|
if (
|
||||||
!chapter.read &&
|
!chapter.read &&
|
||||||
chapter.isRecognizedNumber &&
|
chapter.isRecognizedNumber &&
|
||||||
chapter.chapter_number == readerChapter.chapter.chapter_number
|
chapter.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
|
||||||
) {
|
) {
|
||||||
ChapterUpdate(id = chapter.id!!, read = true)
|
ChapterUpdate(id = chapter.id, read = true)
|
||||||
// SY -->
|
// SY -->
|
||||||
.also { deleteChapterIfNeeded(ReaderChapter(chapter)) }
|
.also { deleteChapterIfNeeded(ReaderChapter(chapter)) }
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -1068,7 +1071,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
(state.value.dialog as? Dialog.PageActions)?.page
|
(state.value.dialog as? Dialog.PageActions)?.page
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
if (page?.status != Page.State.READY) return
|
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>()
|
||||||
@@ -1112,8 +1115,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
||||||
val bg = viewer.config.pageCanvasColor
|
val bg = viewer.config.pageCanvasColor
|
||||||
|
|
||||||
if (firstPage.status != Page.State.READY) return
|
if (firstPage.status != Page.State.Ready) return
|
||||||
if (secondPage?.status != Page.State.READY) return
|
if (secondPage?.status != Page.State.Ready) return
|
||||||
|
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
|
|
||||||
@@ -1188,7 +1191,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
(state.value.dialog as? Dialog.PageActions)?.page
|
(state.value.dialog as? Dialog.PageActions)?.page
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
if (page?.status != Page.State.READY) return
|
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>()
|
||||||
@@ -1220,8 +1223,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
|
||||||
val bg = viewer.config.pageCanvasColor
|
val bg = viewer.config.pageCanvasColor
|
||||||
|
|
||||||
if (firstPage.status != Page.State.READY) return
|
if (firstPage.status != Page.State.Ready) return
|
||||||
if (secondPage?.status != Page.State.READY) return
|
if (secondPage?.status != Page.State.Ready) return
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
|
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
@@ -1257,7 +1260,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
(state.value.dialog as? Dialog.PageActions)?.page
|
(state.value.dialog as? Dialog.PageActions)?.page
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
if (page?.status != Page.State.READY) return
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader
|
|||||||
// SY -->
|
// SY -->
|
||||||
stream = { imageBytes?.copyOf()?.inputStream() ?: reader.getInputStream(entry.name)!! }
|
stream = { imageBytes?.copyOf()?.inputStream() ?: reader.getInputStream(entry.name)!! }
|
||||||
// SY <--
|
// SY <--
|
||||||
status = Page.State.READY
|
status = Page.State.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
|
|||||||
val streamFn = { file.openInputStream() }
|
val streamFn = { file.openInputStream() }
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
stream = streamFn
|
stream = streamFn
|
||||||
status = Page.State.READY
|
status = Page.State.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ internal class DownloadPageLoader(
|
|||||||
ReaderPage(page.index, page.url, page.imageUrl) {
|
ReaderPage(page.index, page.url, page.imageUrl) {
|
||||||
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
|
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
|
||||||
}.apply {
|
}.apply {
|
||||||
status = Page.State.READY
|
status = Page.State.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
|
|||||||
val streamFn = { epub.getInputStream(path)!! }
|
val streamFn = { epub.getInputStream(path)!! }
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
stream = streamFn
|
stream = streamFn
|
||||||
status = Page.State.READY
|
status = Page.State.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import tachiyomi.core.common.util.lang.withIOContext
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.PriorityBlockingQueue
|
import java.util.concurrent.PriorityBlockingQueue
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.incrementAndFetch
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +68,7 @@ internal class HttpPageLoader(
|
|||||||
emit(runInterruptible { queue.take() }.page)
|
emit(runInterruptible { queue.take() }.page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.filter { it.status == Page.State.QUEUE }
|
.filter { it.status == Page.State.Queue }
|
||||||
.collect(::internalLoadPage)
|
.collect(::internalLoadPage)
|
||||||
}
|
}
|
||||||
// EXH -->
|
// EXH -->
|
||||||
@@ -96,7 +98,7 @@ internal class HttpPageLoader(
|
|||||||
}
|
}
|
||||||
if (readerPreferences.aggressivePageLoading().get()) {
|
if (readerPreferences.aggressivePageLoading().get()) {
|
||||||
rp.forEach {
|
rp.forEach {
|
||||||
if (it.status == Page.State.QUEUE) {
|
if (it.status == Page.State.Queue) {
|
||||||
queue.offer(PriorityPage(it, 0))
|
queue.offer(PriorityPage(it, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,17 +114,17 @@ internal class HttpPageLoader(
|
|||||||
val imageUrl = page.imageUrl
|
val imageUrl = page.imageUrl
|
||||||
|
|
||||||
// Check if the image has been deleted
|
// Check if the image has been deleted
|
||||||
if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) {
|
if (page.status == Page.State.Ready && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) {
|
||||||
page.status = Page.State.QUEUE
|
page.status = Page.State.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically retry failed pages when subscribed to this page
|
// Automatically retry failed pages when subscribed to this page
|
||||||
if (page.status == Page.State.ERROR) {
|
if (page.status is Page.State.Error) {
|
||||||
page.status = Page.State.QUEUE
|
page.status = Page.State.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
val queuedPages = mutableListOf<PriorityPage>()
|
val queuedPages = mutableListOf<PriorityPage>()
|
||||||
if (page.status == Page.State.QUEUE) {
|
if (page.status == Page.State.Queue) {
|
||||||
queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
|
queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
|
||||||
}
|
}
|
||||||
queuedPages += preloadNextPages(page, preloadSize)
|
queuedPages += preloadNextPages(page, preloadSize)
|
||||||
@@ -130,7 +132,7 @@ internal class HttpPageLoader(
|
|||||||
suspendCancellableCoroutine<Nothing> { continuation ->
|
suspendCancellableCoroutine<Nothing> { continuation ->
|
||||||
continuation.invokeOnCancellation {
|
continuation.invokeOnCancellation {
|
||||||
queuedPages.forEach {
|
queuedPages.forEach {
|
||||||
if (it.page.status == Page.State.QUEUE) {
|
if (it.page.status == Page.State.Queue) {
|
||||||
queue.remove(it)
|
queue.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,8 +144,8 @@ internal class HttpPageLoader(
|
|||||||
* Retries a page. This method is only called from user interaction on the viewer.
|
* Retries a page. This method is only called from user interaction on the viewer.
|
||||||
*/
|
*/
|
||||||
override fun retryPage(page: ReaderPage) {
|
override fun retryPage(page: ReaderPage) {
|
||||||
if (page.status == Page.State.ERROR) {
|
if (page.status is Page.State.Error) {
|
||||||
page.status = Page.State.QUEUE
|
page.status = Page.State.Queue
|
||||||
}
|
}
|
||||||
// EXH -->
|
// EXH -->
|
||||||
// Grab a new image URL on EXH sources
|
// Grab a new image URL on EXH sources
|
||||||
@@ -194,7 +196,7 @@ internal class HttpPageLoader(
|
|||||||
return pages
|
return pages
|
||||||
.subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size))
|
.subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size))
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
if (it.status == Page.State.QUEUE) {
|
if (it.status == Page.State.Queue) {
|
||||||
PriorityPage(it, 0).apply { queue.offer(this) }
|
PriorityPage(it, 0).apply { queue.offer(this) }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -211,21 +213,21 @@ internal class HttpPageLoader(
|
|||||||
private suspend fun internalLoadPage(page: ReaderPage) {
|
private suspend fun internalLoadPage(page: ReaderPage) {
|
||||||
try {
|
try {
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LoadPage
|
||||||
page.imageUrl = source.getImageUrl(page)
|
page.imageUrl = source.getImageUrl(page)
|
||||||
}
|
}
|
||||||
val imageUrl = page.imageUrl!!
|
val imageUrl = page.imageUrl!!
|
||||||
|
|
||||||
if (!chapterCache.isImageInCache(imageUrl)) {
|
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||||
page.status = Page.State.DOWNLOAD_IMAGE
|
page.status = Page.State.DownloadImage
|
||||||
val imageResponse = source.getImage(page, dataSaver)
|
val imageResponse = source.getImage(page, dataSaver)
|
||||||
chapterCache.putImageToCache(imageUrl, imageResponse)
|
chapterCache.putImageToCache(imageUrl, imageResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||||
page.status = Page.State.READY
|
page.status = Page.State.Ready
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
page.status = Page.State.Error(e)
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -234,7 +236,7 @@ internal class HttpPageLoader(
|
|||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
fun boostPage(page: ReaderPage) {
|
fun boostPage(page: ReaderPage) {
|
||||||
if (page.status == Page.State.QUEUE) {
|
if (page.status == Page.State.Queue) {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
loadPage(page)
|
loadPage(page)
|
||||||
}
|
}
|
||||||
@@ -246,15 +248,16 @@ internal class HttpPageLoader(
|
|||||||
/**
|
/**
|
||||||
* Data class used to keep ordering of pages in order to maintain priority.
|
* Data class used to keep ordering of pages in order to maintain priority.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private class PriorityPage(
|
private class PriorityPage(
|
||||||
val page: ReaderPage,
|
val page: ReaderPage,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
) : Comparable<PriorityPage> {
|
) : Comparable<PriorityPage> {
|
||||||
companion object {
|
companion object {
|
||||||
private val idGenerator = AtomicInteger()
|
private val idGenerator = AtomicInt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val identifier = idGenerator.incrementAndGet()
|
private val identifier = idGenerator.incrementAndFetch()
|
||||||
|
|
||||||
override fun compareTo(other: PriorityPage): Int {
|
override fun compareTo(other: PriorityPage): Int {
|
||||||
val p = other.priority.compareTo(priority)
|
val p = other.priority.compareTo(priority)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class InsertPage(val parent: ReaderPage) : ReaderPage(parent.index, parent.url,
|
|||||||
override var chapter: ReaderChapter = parent.chapter
|
override var chapter: ReaderChapter = parent.chapter
|
||||||
|
|
||||||
init {
|
init {
|
||||||
status = State.READY
|
status = State.Ready
|
||||||
stream = parent.stream
|
stream = parent.stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ReaderPreferences(
|
|||||||
|
|
||||||
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
|
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
|
||||||
|
|
||||||
fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true)
|
fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", false)
|
||||||
|
|
||||||
fun defaultReadingMode() = preferenceStore.getInt(
|
fun defaultReadingMode() = preferenceStore.getInt(
|
||||||
"pref_default_reading_mode_key",
|
"pref_default_reading_mode_key",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
private var config: Config? = null
|
private var config: Config? = null
|
||||||
|
|
||||||
var onImageLoaded: (() -> Unit)? = null
|
var onImageLoaded: (() -> Unit)? = null
|
||||||
var onImageLoadError: (() -> Unit)? = null
|
var onImageLoadError: ((Throwable?) -> Unit)? = null
|
||||||
var onScaleChanged: ((newScale: Float) -> Unit)? = null
|
var onScaleChanged: ((newScale: Float) -> Unit)? = null
|
||||||
var onViewClicked: (() -> Unit)? = null
|
var onViewClicked: (() -> Unit)? = null
|
||||||
|
|
||||||
@@ -85,8 +85,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onImageLoadError() {
|
open fun onImageLoadError(error: Throwable?) {
|
||||||
onImageLoadError?.invoke()
|
onImageLoadError?.invoke(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@@ -114,7 +114,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoadError(e: Exception) {
|
override fun onImageLoadError(e: Exception) {
|
||||||
onImageLoadError()
|
onImageLoadError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -290,7 +290,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoadError(e: Exception) {
|
override fun onImageLoadError(e: Exception) {
|
||||||
this@ReaderPageImageView.onImageLoadError()
|
this@ReaderPageImageView.onImageLoadError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -318,8 +318,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
setImage(ImageSource.bitmap(image.bitmap))
|
setImage(ImageSource.bitmap(image.bitmap))
|
||||||
isVisible = true
|
isVisible = true
|
||||||
},
|
},
|
||||||
onError = {
|
)
|
||||||
onImageLoadError()
|
.listener(
|
||||||
|
onError = { _, result ->
|
||||||
|
onImageLoadError(result.throwable)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.size(ViewSizeResolver(this@ReaderPageImageView))
|
.size(ViewSizeResolver(this@ReaderPageImageView))
|
||||||
@@ -395,8 +397,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
isVisible = true
|
isVisible = true
|
||||||
this@ReaderPageImageView.onImageLoaded()
|
this@ReaderPageImageView.onImageLoaded()
|
||||||
},
|
},
|
||||||
onError = {
|
)
|
||||||
this@ReaderPageImageView.onImageLoadError()
|
.listener(
|
||||||
|
onError = { _, result ->
|
||||||
|
onImageLoadError(result.throwable)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.crossfade(false)
|
.crossfade(false)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import eu.kanade.presentation.util.formattedMessage
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
@@ -22,12 +23,14 @@ import kotlinx.coroutines.supervisorScope
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,16 +115,16 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
page.statusFlow.collectLatest { state ->
|
page.statusFlow.collectLatest { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
Page.State.QUEUE -> setQueued()
|
Page.State.Queue -> setQueued()
|
||||||
Page.State.LOAD_PAGE -> setLoading()
|
Page.State.LoadPage -> setLoading()
|
||||||
Page.State.DOWNLOAD_IMAGE -> {
|
Page.State.DownloadImage -> {
|
||||||
setDownloading()
|
setDownloading()
|
||||||
page.progressFlow.collectLatest { value ->
|
page.progressFlow.collectLatest { value ->
|
||||||
progressIndicator?.setProgress(value)
|
progressIndicator?.setProgress(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page.State.READY -> setImage()
|
Page.State.Ready -> setImage()
|
||||||
Page.State.ERROR -> setError()
|
is Page.State.Error -> setError(state.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +216,7 @@ class PagerPageHolder(
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext {
|
withUIContext {
|
||||||
setError()
|
setError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,9 +408,9 @@ class PagerPageHolder(
|
|||||||
/**
|
/**
|
||||||
* Called when the page has an error.
|
* Called when the page has an error.
|
||||||
*/
|
*/
|
||||||
private fun setError() {
|
private fun setError(error: Throwable?) {
|
||||||
progressIndicator?.hide()
|
progressIndicator?.hide()
|
||||||
showErrorLayout()
|
showErrorLayout(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoaded() {
|
override fun onImageLoaded() {
|
||||||
@@ -418,9 +421,9 @@ class PagerPageHolder(
|
|||||||
/**
|
/**
|
||||||
* Called when an image fails to decode.
|
* Called when an image fails to decode.
|
||||||
*/
|
*/
|
||||||
override fun onImageLoadError() {
|
override fun onImageLoadError(error: Throwable?) {
|
||||||
super.onImageLoadError()
|
super.onImageLoadError(error)
|
||||||
setError()
|
setError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -431,7 +434,7 @@ class PagerPageHolder(
|
|||||||
viewer.activity.hideMenu()
|
viewer.activity.hideMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showErrorLayout(): ReaderErrorBinding {
|
private fun showErrorLayout(error: Throwable?): ReaderErrorBinding {
|
||||||
if (errorLayout == null) {
|
if (errorLayout == null) {
|
||||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
errorLayout?.actionRetry?.viewer = viewer
|
errorLayout?.actionRetry?.viewer = viewer
|
||||||
@@ -446,12 +449,17 @@ class PagerPageHolder(
|
|||||||
if (imageUrl.startsWith("http", true)) {
|
if (imageUrl.startsWith("http", true)) {
|
||||||
errorLayout?.actionOpenInWebView?.viewer = viewer
|
errorLayout?.actionOpenInWebView?.viewer = viewer
|
||||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||||
val intent = WebViewActivity.newIntent(context, imageUrl)
|
val sourceId = viewer.activity.viewModel.manga?.source
|
||||||
|
|
||||||
|
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
|
||||||
|
?: context.stringResource(MR.strings.decode_image_error)
|
||||||
|
|
||||||
errorLayout?.root?.isVisible = true
|
errorLayout?.root?.isVisible = true
|
||||||
return errorLayout!!
|
return errorLayout!!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,11 +105,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
pager.offscreenPageLimit = 1
|
pager.offscreenPageLimit = 1
|
||||||
pager.id = R.id.reader_pager
|
pager.id = R.id.reader_pager
|
||||||
pager.adapter = adapter
|
pager.adapter = adapter
|
||||||
pager.addOnPageChangeListener(
|
pager.addOnPageChangeListener(pagerListener)
|
||||||
// SY -->
|
|
||||||
pagerListener,
|
|
||||||
// SY <--
|
|
||||||
)
|
|
||||||
pager.tapListener = { event ->
|
pager.tapListener = { event ->
|
||||||
val viewPosition = IntArray(2)
|
val viewPosition = IntArray(2)
|
||||||
pager.getLocationOnScreen(viewPosition)
|
pager.getLocationOnScreen(viewPosition)
|
||||||
@@ -291,6 +287,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
* Sets the active [chapters] on this pager.
|
* Sets the active [chapters] on this pager.
|
||||||
*/
|
*/
|
||||||
private fun setChaptersInternal(chapters: ViewerChapters) {
|
private fun setChaptersInternal(chapters: ViewerChapters) {
|
||||||
|
// Remove listener so the change in item doesn't trigger it
|
||||||
|
pager.removeOnPageChangeListener(pagerListener)
|
||||||
|
|
||||||
val forceTransition =
|
val forceTransition =
|
||||||
config.alwaysShowChapterTransition ||
|
config.alwaysShowChapterTransition ||
|
||||||
adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition
|
adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition
|
||||||
@@ -303,6 +302,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
moveToPage(pages[min(chapters.currChapter.requestedPage, pages.lastIndex)])
|
moveToPage(pages[min(chapters.currChapter.requestedPage, pages.lastIndex)])
|
||||||
pager.isVisible = true
|
pager.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pager.addOnPageChangeListener(pagerListener)
|
||||||
|
// Manually call onPageChange to update the UI
|
||||||
|
onPageChange(pager.currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -473,13 +476,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun setChaptersDoubleShift(chapters: ViewerChapters) {
|
fun setChaptersDoubleShift(chapters: ViewerChapters) {
|
||||||
// Remove Listener since we're about to change the size of the items
|
|
||||||
// If we don't the size change could put us on a new chapter
|
|
||||||
pager.removeOnPageChangeListener(pagerListener)
|
|
||||||
setChaptersInternal(chapters)
|
setChaptersInternal(chapters)
|
||||||
pager.addOnPageChangeListener(pagerListener)
|
|
||||||
// Since we removed the listener while shifting, call page change to update the ui
|
|
||||||
onPageChange(pager.currentItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateShifting(page: ReaderPage? = null) {
|
fun updateShifting(page: ReaderPage? = null) {
|
||||||
|
|||||||
+19
-11
@@ -10,6 +10,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
import eu.kanade.presentation.util.formattedMessage
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
@@ -25,11 +26,13 @@ import kotlinx.coroutines.supervisorScope
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holder of the webtoon reader for a single page of a chapter.
|
* Holder of the webtoon reader for a single page of a chapter.
|
||||||
@@ -81,7 +84,7 @@ class WebtoonPageHolder(
|
|||||||
refreshLayoutParams()
|
refreshLayoutParams()
|
||||||
|
|
||||||
frame.onImageLoaded = { onImageDecoded() }
|
frame.onImageLoaded = { onImageDecoded() }
|
||||||
frame.onImageLoadError = { setError() }
|
frame.onImageLoadError = { error -> setError(error) }
|
||||||
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
frame.onScaleChanged = { viewer.activity.hideMenu() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,16 +139,16 @@ class WebtoonPageHolder(
|
|||||||
}
|
}
|
||||||
page.statusFlow.collectLatest { state ->
|
page.statusFlow.collectLatest { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
Page.State.QUEUE -> setQueued()
|
Page.State.Queue -> setQueued()
|
||||||
Page.State.LOAD_PAGE -> setLoading()
|
Page.State.LoadPage -> setLoading()
|
||||||
Page.State.DOWNLOAD_IMAGE -> {
|
Page.State.DownloadImage -> {
|
||||||
setDownloading()
|
setDownloading()
|
||||||
page.progressFlow.collectLatest { value ->
|
page.progressFlow.collectLatest { value ->
|
||||||
progressIndicator.setProgress(value)
|
progressIndicator.setProgress(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page.State.READY -> setImage()
|
Page.State.Ready -> setImage()
|
||||||
Page.State.ERROR -> setError()
|
is Page.State.Error -> setError(state.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +212,7 @@ class WebtoonPageHolder(
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext {
|
withUIContext {
|
||||||
setError()
|
setError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,9 +246,9 @@ class WebtoonPageHolder(
|
|||||||
/**
|
/**
|
||||||
* Called when the page has an error.
|
* Called when the page has an error.
|
||||||
*/
|
*/
|
||||||
private fun setError() {
|
private fun setError(error: Throwable?) {
|
||||||
progressContainer.isVisible = false
|
progressContainer.isVisible = false
|
||||||
initErrorLayout()
|
initErrorLayout(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,7 +278,7 @@ class WebtoonPageHolder(
|
|||||||
/**
|
/**
|
||||||
* Initializes a button to retry pages.
|
* Initializes a button to retry pages.
|
||||||
*/
|
*/
|
||||||
private fun initErrorLayout(): ReaderErrorBinding {
|
private fun initErrorLayout(error: Throwable?): ReaderErrorBinding {
|
||||||
if (errorLayout == null) {
|
if (errorLayout == null) {
|
||||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
||||||
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
||||||
@@ -289,12 +292,17 @@ class WebtoonPageHolder(
|
|||||||
if (imageUrl != null) {
|
if (imageUrl != null) {
|
||||||
if (imageUrl.startsWith("http", true)) {
|
if (imageUrl.startsWith("http", true)) {
|
||||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||||
val intent = WebViewActivity.newIntent(context, imageUrl)
|
val sourceId = viewer.activity.viewModel.manga?.source
|
||||||
|
|
||||||
|
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
|
||||||
|
?: context.stringResource(MR.strings.decode_image_error)
|
||||||
|
|
||||||
return errorLayout!!
|
return errorLayout!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,19 +40,19 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsMainScreen
|
else -> SettingsMainScreen
|
||||||
},
|
},
|
||||||
content = {
|
onBackPressed = null,
|
||||||
val pop: () -> Unit = {
|
) {
|
||||||
if (it.canPop) {
|
val pop: () -> Unit = {
|
||||||
it.pop()
|
if (it.canPop) {
|
||||||
} else {
|
it.pop()
|
||||||
parentNavigator.pop()
|
} else {
|
||||||
}
|
parentNavigator.pop()
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(LocalBackPress provides pop) {
|
}
|
||||||
DefaultNavigatorScreenTransition(navigator = it)
|
CompositionLocalProvider(LocalBackPress provides pop) {
|
||||||
}
|
DefaultNavigatorScreenTransition(navigator = it)
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator(
|
Navigator(
|
||||||
screen = when (destination) {
|
screen = when (destination) {
|
||||||
@@ -61,6 +61,7 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsAppearanceScreen
|
else -> SettingsAppearanceScreen
|
||||||
},
|
},
|
||||||
|
onBackPressed = null,
|
||||||
) {
|
) {
|
||||||
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
||||||
TwoPanelBox(
|
TwoPanelBox(
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ package eu.kanade.tachiyomi.util.view
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.RoundedCorner
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -95,3 +97,22 @@ fun View?.isVisibleOnScreen(): Boolean {
|
|||||||
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
||||||
return actualPosition.intersect(screen)
|
return actualPosition.intersect(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns window radius (in pixel) applied to this view
|
||||||
|
*/
|
||||||
|
fun View.getWindowRadius(): Int {
|
||||||
|
val rad = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val windowInsets = rootWindowInsets
|
||||||
|
listOfNotNull(
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT),
|
||||||
|
)
|
||||||
|
.minOfOrNull { it.radius }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return rad ?: 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class GalleryAdder(
|
|||||||
|
|
||||||
// Use manga in DB if possible, otherwise, make a new manga
|
// Use manga in DB if possible, otherwise, make a new manga
|
||||||
var manga = getManga.await(cleanedMangaUrl, source.id)
|
var manga = getManga.await(cleanedMangaUrl, source.id)
|
||||||
?: networkToLocalManga.await(
|
?: networkToLocalManga(
|
||||||
Manga.create().copy(
|
Manga.create().copy(
|
||||||
source = source.id,
|
source = source.id,
|
||||||
url = cleanedMangaUrl,
|
url = cleanedMangaUrl,
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import androidx.compose.animation.Crossfade
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.forEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -201,13 +199,7 @@ class SettingsDebugScreen : Screen() {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(color = Color.White.copy(alpha = 0.3F))
|
.background(color = Color.White.copy(alpha = 0.3F))
|
||||||
.pointerInput(running && result == null) {
|
.pointerInput(running && result == null) {},
|
||||||
forEachGesture {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
waitForUpOrCancellation()?.consume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package exh.favorites
|
package exh.favorites
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||||
import exh.source.EXH_SOURCE_ID
|
import exh.source.EXH_SOURCE_ID
|
||||||
@@ -11,6 +10,7 @@ import kotlinx.coroutines.flow.filter
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
|
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package exh.md.follows
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import tachiyomi.data.source.SourcePagingSource
|
import tachiyomi.data.source.BaseSourcePagingSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LatestUpdatesPager inherited from the general Pager.
|
* LatestUpdatesPager inherited from the general Pager.
|
||||||
*/
|
*/
|
||||||
class MangaDexFollowsPagingSource(val mangadex: MangaDex) : SourcePagingSource(mangadex) {
|
class MangaDexFollowsPagingSource(val mangadex: MangaDex) : BaseSourcePagingSource(mangadex) {
|
||||||
|
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||||
return mangadex.fetchFollows(currentPage)
|
return mangadex.fetchFollows(currentPage)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
|
|||||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
||||||
when {
|
when {
|
||||||
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
||||||
duplicateManga != null -> screenModel.setDialog(
|
duplicateManga.isNotEmpty() -> screenModel.setDialog(
|
||||||
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
||||||
manga,
|
manga,
|
||||||
duplicateManga,
|
duplicateManga,
|
||||||
@@ -103,14 +103,15 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
|
|||||||
is BrowseSourceScreenModel.Dialog.Migrate -> {}
|
is BrowseSourceScreenModel.Dialog.Migrate -> {}
|
||||||
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
|
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||||
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||||
onMigrate = {
|
onMigrate = {
|
||||||
PreMigrationScreen.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
navigator,
|
navigator,
|
||||||
dialog.duplicate.id,
|
it.id,
|
||||||
dialog.manga.id,
|
dialog.manga.id,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import exh.source.getMainSource
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.data.source.BaseSourcePagingSource
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.repository.SourcePagingSourceType
|
|
||||||
|
|
||||||
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
|
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
|
||||||
|
|
||||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
override fun createSourcePagingSource(query: String, filters: FilterList): BaseSourcePagingSource {
|
||||||
return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex)
|
return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.presentation.util.ioCoroutineScope
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import exh.recs.sources.RecommendationPagingSource
|
import exh.recs.sources.RecommendationPagingSource
|
||||||
@@ -23,6 +22,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -85,7 +85,7 @@ open class RecommendsScreenModel(
|
|||||||
val recSourceId = recSource.associatedSourceId
|
val recSourceId = recSource.associatedSourceId
|
||||||
if (recSourceId != null) {
|
if (recSourceId != null) {
|
||||||
// If the recommendation is associated with a source, resolve it
|
// If the recommendation is associated with a source, resolve it
|
||||||
networkToLocalManga.await(it.toDomainManga(recSourceId))
|
networkToLocalManga(it.toDomainManga(recSourceId))
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch
|
// Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch
|
||||||
it.toDomainManga(-1)
|
it.toDomainManga(-1)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.net.wifi.WifiManager
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@@ -26,6 +25,7 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.data.source.NoResultsException
|
import tachiyomi.data.source.NoResultsException
|
||||||
import tachiyomi.domain.UnsortedPreferences
|
import tachiyomi.domain.UnsortedPreferences
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
@@ -201,8 +201,7 @@ class RecommendationSearchHelper(val context: Context) {
|
|||||||
return filterNot { manga ->
|
return filterNot { manga ->
|
||||||
// Source recommendations can be directly resolved, if the recommendation is from the same source
|
// Source recommendations can be directly resolved, if the recommendation is from the same source
|
||||||
recSource.associatedSourceId?.let { srcId ->
|
recSource.associatedSourceId?.let { srcId ->
|
||||||
return@filterNot networkToLocalManga
|
return@filterNot networkToLocalManga(manga.toDomainManga(srcId))
|
||||||
.await(manga.toDomainManga(srcId))
|
|
||||||
.let { local -> libraryManga.any { it.id == local.id } }
|
.let { local -> libraryManga.any { it.id == local.id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import exh.source.isMdBasedSource
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.data.source.BaseSourcePagingSource
|
||||||
import tachiyomi.data.source.NoResultsException
|
import tachiyomi.data.source.NoResultsException
|
||||||
import tachiyomi.data.source.SourcePagingSource
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
@@ -29,7 +29,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
abstract class RecommendationPagingSource(
|
abstract class RecommendationPagingSource(
|
||||||
protected val manga: Manga,
|
protected val manga: Manga,
|
||||||
source: CatalogueSource? = null,
|
source: CatalogueSource? = null,
|
||||||
) : SourcePagingSource(source) {
|
) : BaseSourcePagingSource(source) {
|
||||||
// Display name
|
// Display name
|
||||||
abstract val name: String
|
abstract val name: String
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package exh.smartsearch
|
package exh.smartsearch
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.toDomainManga
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
||||||
class SmartSourceSearchEngine(
|
class SmartSourceSearchEngine(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class SmartSearchScreenModel(
|
|||||||
val result = try {
|
val result = try {
|
||||||
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
||||||
if (resultManga != null) {
|
if (resultManga != null) {
|
||||||
val localManga = networkToLocalManga.await(resultManga)
|
val localManga = networkToLocalManga(resultManga)
|
||||||
SearchResults.Found(localManga)
|
SearchResults.Found(localManga)
|
||||||
} else {
|
} else {
|
||||||
SearchResults.NotFound
|
SearchResults.NotFound
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded"
|
app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded,mig_notes"
|
||||||
app:flow_horizontalBias="0"
|
app:flow_horizontalBias="0"
|
||||||
app:flow_horizontalGap="8dp"
|
app:flow_horizontalGap="8dp"
|
||||||
app:flow_horizontalStyle="packed"
|
app:flow_horizontalStyle="packed"
|
||||||
@@ -94,6 +94,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="@string/delete_downloaded" />
|
android:text="@string/delete_downloaded" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_notes"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/action_notes" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
android:gravity="center">
|
android:gravity="center">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/error_message"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package mihon.buildlogic
|
package mihon.buildlogic
|
||||||
|
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@@ -19,9 +20,18 @@ fun Project.getGitSha(): String {
|
|||||||
|
|
||||||
private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
/**
|
||||||
fun Project.getBuildTime(): String {
|
* @param useLastCommitTime If `true`, the build time is based on the timestamp of the last Git commit;
|
||||||
return LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
|
* otherwise, the current time is used. Both are in UTC.
|
||||||
|
* @return A formatted string representing the build time. The format used is defined by [BUILD_TIME_FORMATTER].
|
||||||
|
*/
|
||||||
|
fun Project.getBuildTime(useLastCommitTime: Boolean): String {
|
||||||
|
return if (useLastCommitTime) {
|
||||||
|
val epoch = runCommand("git log -1 --format=%ct").toLong()
|
||||||
|
Instant.ofEpochSecond(epoch).atOffset(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
|
||||||
|
} else {
|
||||||
|
LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Project.runCommand(command: String): String {
|
private fun Project.runCommand(command: String): String {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ dependencies {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
implementation(sylibs.xlog)
|
implementation(sylibs.xlog)
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt)
|
||||||
implementation(sylibs.exifinterface)
|
implementation(sylibs.exifinterface)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class NetworkPreferences(
|
|||||||
fun defaultUserAgent(): Preference<String> {
|
fun defaultUserAgent(): Preference<String> {
|
||||||
return preferenceStore.getString(
|
return preferenceStore.getString(
|
||||||
"default_user_agent",
|
"default_user_agent",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
|
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,20 +16,23 @@ import rx.Observable
|
|||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
val call = clone()
|
val call = clone()
|
||||||
|
|
||||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
val requestArbiter = object : Producer, Subscription {
|
||||||
|
val boolean = AtomicBoolean(false)
|
||||||
override fun request(n: Long) {
|
override fun request(n: Long) {
|
||||||
if (n == 0L || !compareAndSet(false, true)) return
|
if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val response = call.execute()
|
val response = call.execute()
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ private val mapper = { cursor: SqlCursor ->
|
|||||||
favorite_modified_at = cursor.getLong(22),
|
favorite_modified_at = cursor.getLong(22),
|
||||||
version = cursor.getLong(23)!!,
|
version = cursor.getLong(23)!!,
|
||||||
is_syncing = cursor.getLong(24)!!,
|
is_syncing = cursor.getLong(24)!!,
|
||||||
totalCount = cursor.getLong(25)!!,
|
notes = cursor.getString(25)!!,
|
||||||
readCount = cursor.getDouble(26)!!,
|
totalCount = cursor.getLong(26)!!,
|
||||||
latestUpload = cursor.getLong(27)!!,
|
readCount = cursor.getDouble(27)!!,
|
||||||
chapterFetchedAt = cursor.getLong(28)!!,
|
latestUpload = cursor.getLong(28)!!,
|
||||||
lastRead = cursor.getLong(29)!!,
|
chapterFetchedAt = cursor.getLong(29)!!,
|
||||||
bookmarkCount = cursor.getDouble(30)!!,
|
lastRead = cursor.getLong(30)!!,
|
||||||
category = cursor.getLong(31)!!,
|
bookmarkCount = cursor.getDouble(31)!!,
|
||||||
|
category = cursor.getLong(32)!!,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.concurrent.RejectedExecutionException
|
import java.util.concurrent.RejectedExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlin.concurrent.atomics.AtomicInt
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.decrementAndFetch
|
||||||
|
import kotlin.concurrent.atomics.incrementAndFetch
|
||||||
import kotlin.coroutines.ContinuationInterceptor
|
import kotlin.coroutines.ContinuationInterceptor
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
@@ -127,6 +130,7 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
|||||||
/**
|
/**
|
||||||
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private class TransactionElement(
|
private class TransactionElement(
|
||||||
private val transactionThreadControlJob: Job,
|
private val transactionThreadControlJob: Job,
|
||||||
val transactionDispatcher: ContinuationInterceptor,
|
val transactionDispatcher: ContinuationInterceptor,
|
||||||
@@ -143,14 +147,14 @@ private class TransactionElement(
|
|||||||
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
||||||
* is released.
|
* is released.
|
||||||
*/
|
*/
|
||||||
private val referenceCount = AtomicInteger(0)
|
private val referenceCount = AtomicInt(0)
|
||||||
|
|
||||||
fun acquire() {
|
fun acquire() {
|
||||||
referenceCount.incrementAndGet()
|
referenceCount.incrementAndFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
val count = referenceCount.decrementAndGet()
|
val count = referenceCount.decrementAndFetch()
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
throw IllegalStateException("Transaction was never started or was already released")
|
throw IllegalStateException("Transaction was never started or was already released")
|
||||||
} else if (count == 0) {
|
} else if (count == 0) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tachiyomi.data.manga
|
|||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
import tachiyomi.view.LibraryView
|
import tachiyomi.view.LibraryView
|
||||||
|
|
||||||
object MangaMapper {
|
object MangaMapper {
|
||||||
@@ -36,6 +37,7 @@ object MangaMapper {
|
|||||||
version: Long,
|
version: Long,
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
|
notes: String,
|
||||||
): Manga = Manga(
|
): Manga = Manga(
|
||||||
id = id,
|
id = id,
|
||||||
source = source,
|
source = source,
|
||||||
@@ -62,6 +64,7 @@ object MangaMapper {
|
|||||||
lastModifiedAt = lastModifiedAt,
|
lastModifiedAt = lastModifiedAt,
|
||||||
favoriteModifiedAt = favoriteModifiedAt,
|
favoriteModifiedAt = favoriteModifiedAt,
|
||||||
version = version,
|
version = version,
|
||||||
|
notes = notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mapLibraryManga(
|
fun mapLibraryManga(
|
||||||
@@ -93,6 +96,7 @@ object MangaMapper {
|
|||||||
favoriteModifiedAt: Long?,
|
favoriteModifiedAt: Long?,
|
||||||
version: Long,
|
version: Long,
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
|
notes: String,
|
||||||
totalCount: Long,
|
totalCount: Long,
|
||||||
readCount: Double,
|
readCount: Double,
|
||||||
latestUpload: Long,
|
latestUpload: Long,
|
||||||
@@ -129,6 +133,7 @@ object MangaMapper {
|
|||||||
favoriteModifiedAt,
|
favoriteModifiedAt,
|
||||||
version,
|
version,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
|
notes,
|
||||||
),
|
),
|
||||||
category = category,
|
category = category,
|
||||||
totalChapters = totalCount,
|
totalChapters = totalCount,
|
||||||
@@ -139,6 +144,71 @@ object MangaMapper {
|
|||||||
lastRead = lastRead,
|
lastRead = lastRead,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun mapMangaWithChapterCount(
|
||||||
|
id: Long,
|
||||||
|
source: Long,
|
||||||
|
url: String,
|
||||||
|
artist: String?,
|
||||||
|
author: String?,
|
||||||
|
description: String?,
|
||||||
|
genre: List<String>?,
|
||||||
|
title: String,
|
||||||
|
status: Long,
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
favorite: Boolean,
|
||||||
|
lastUpdate: Long?,
|
||||||
|
nextUpdate: Long?,
|
||||||
|
initialized: Boolean,
|
||||||
|
viewerFlags: Long,
|
||||||
|
chapterFlags: Long,
|
||||||
|
coverLastModified: Long,
|
||||||
|
dateAdded: Long,
|
||||||
|
// SY -->
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
filteredScanlators: String?,
|
||||||
|
// SY <--
|
||||||
|
updateStrategy: UpdateStrategy,
|
||||||
|
calculateInterval: Long,
|
||||||
|
lastModifiedAt: Long,
|
||||||
|
favoriteModifiedAt: Long?,
|
||||||
|
version: Long,
|
||||||
|
isSyncing: Long,
|
||||||
|
notes: String,
|
||||||
|
totalCount: Long,
|
||||||
|
): MangaWithChapterCount = MangaWithChapterCount(
|
||||||
|
manga = mapManga(
|
||||||
|
id,
|
||||||
|
source,
|
||||||
|
url,
|
||||||
|
artist,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
genre,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
thumbnailUrl,
|
||||||
|
favorite,
|
||||||
|
lastUpdate,
|
||||||
|
nextUpdate,
|
||||||
|
initialized,
|
||||||
|
viewerFlags,
|
||||||
|
chapterFlags,
|
||||||
|
coverLastModified,
|
||||||
|
dateAdded,
|
||||||
|
// SY -->
|
||||||
|
null,
|
||||||
|
// SY <--
|
||||||
|
updateStrategy,
|
||||||
|
calculateInterval,
|
||||||
|
lastModifiedAt,
|
||||||
|
favoriteModifiedAt,
|
||||||
|
version,
|
||||||
|
isSyncing,
|
||||||
|
notes,
|
||||||
|
),
|
||||||
|
chapterCount = totalCount,
|
||||||
|
)
|
||||||
|
|
||||||
fun mapLibraryView(libraryView: LibraryView): LibraryManga {
|
fun mapLibraryView(libraryView: LibraryView): LibraryManga {
|
||||||
return LibraryManga(
|
return LibraryManga(
|
||||||
manga = Manga(
|
manga = Manga(
|
||||||
@@ -165,6 +235,7 @@ object MangaMapper {
|
|||||||
lastModifiedAt = libraryView.last_modified_at,
|
lastModifiedAt = libraryView.last_modified_at,
|
||||||
favoriteModifiedAt = libraryView.favorite_modified_at,
|
favoriteModifiedAt = libraryView.favorite_modified_at,
|
||||||
version = libraryView.version,
|
version = libraryView.version,
|
||||||
|
notes = libraryView.notes,
|
||||||
),
|
),
|
||||||
category = libraryView.category,
|
category = libraryView.category,
|
||||||
totalChapters = libraryView.totalCount,
|
totalChapters = libraryView.totalCount,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import tachiyomi.data.UpdateStrategyColumnAdapter
|
|||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -73,9 +74,9 @@ class MangaRepositoryImpl(
|
|||||||
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
|
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga> {
|
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
|
||||||
return handler.awaitList {
|
return handler.awaitList {
|
||||||
mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga)
|
mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,39 +106,6 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insert(manga: Manga): Long? {
|
|
||||||
return handler.awaitOneOrNullExecutable(inTransaction = true) {
|
|
||||||
// SY -->
|
|
||||||
if (mangasQueries.getIdByUrlAndSource(manga.url, manga.source).executeAsOneOrNull() != null) {
|
|
||||||
return@awaitOneOrNullExecutable mangasQueries.getIdByUrlAndSource(manga.url, manga.source)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
mangasQueries.insert(
|
|
||||||
source = manga.source,
|
|
||||||
url = manga.url,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre,
|
|
||||||
title = manga.title,
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
lastUpdate = manga.lastUpdate,
|
|
||||||
nextUpdate = manga.nextUpdate,
|
|
||||||
calculateInterval = manga.fetchInterval.toLong(),
|
|
||||||
initialized = manga.initialized,
|
|
||||||
viewerFlags = manga.viewerFlags,
|
|
||||||
chapterFlags = manga.chapterFlags,
|
|
||||||
coverLastModified = manga.coverLastModified,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
updateStrategy = manga.updateStrategy,
|
|
||||||
version = manga.version,
|
|
||||||
)
|
|
||||||
mangasQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(update: MangaUpdate): Boolean {
|
override suspend fun update(update: MangaUpdate): Boolean {
|
||||||
return try {
|
return try {
|
||||||
partialUpdate(update)
|
partialUpdate(update)
|
||||||
@@ -158,6 +126,44 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
|
||||||
|
return handler.await(inTransaction = true) {
|
||||||
|
manga.map {
|
||||||
|
mangasQueries.insertNetworkManga(
|
||||||
|
source = it.source,
|
||||||
|
url = it.url,
|
||||||
|
// SY -->
|
||||||
|
artist = it.ogArtist,
|
||||||
|
author = it.ogAuthor,
|
||||||
|
description = it.ogDescription,
|
||||||
|
genre = it.ogGenre,
|
||||||
|
title = it.ogTitle,
|
||||||
|
status = it.ogStatus,
|
||||||
|
thumbnailUrl = it.ogThumbnailUrl,
|
||||||
|
// SY <--
|
||||||
|
favorite = it.favorite,
|
||||||
|
lastUpdate = it.lastUpdate,
|
||||||
|
nextUpdate = it.nextUpdate,
|
||||||
|
calculateInterval = it.fetchInterval.toLong(),
|
||||||
|
initialized = it.initialized,
|
||||||
|
viewerFlags = it.viewerFlags,
|
||||||
|
chapterFlags = it.chapterFlags,
|
||||||
|
coverLastModified = it.coverLastModified,
|
||||||
|
dateAdded = it.dateAdded,
|
||||||
|
updateStrategy = it.updateStrategy,
|
||||||
|
version = it.version,
|
||||||
|
// SY -->
|
||||||
|
updateTitle = it.ogTitle.isNotBlank(),
|
||||||
|
updateCover = !it.ogThumbnailUrl.isNullOrBlank(),
|
||||||
|
// SY <--
|
||||||
|
updateDetails = it.initialized,
|
||||||
|
mapper = MangaMapper::mapManga,
|
||||||
|
)
|
||||||
|
.executeAsOne()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
|
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
|
||||||
handler.await(inTransaction = true) {
|
handler.await(inTransaction = true) {
|
||||||
mangaUpdates.forEach { value ->
|
mangaUpdates.forEach { value ->
|
||||||
@@ -184,6 +190,7 @@ class MangaRepositoryImpl(
|
|||||||
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
|
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
|
||||||
version = value.version,
|
version = value.version,
|
||||||
isSyncing = 0,
|
isSyncing = 0,
|
||||||
|
notes = value.notes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,26 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import exh.metadata.metadata.RaisedSearchMetadata
|
import exh.metadata.metadata.RaisedSearchMetadata
|
||||||
|
import mihon.domain.manga.model.toDomainManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
||||||
abstract class EHentaiPagingSource(override val source: CatalogueSource) : SourcePagingSource(source) {
|
abstract class EHentaiPagingSource(
|
||||||
|
override val source: CatalogueSource,
|
||||||
|
) : BaseSourcePagingSource(source) {
|
||||||
|
|
||||||
override fun getPageLoadResult(
|
override suspend fun getPageLoadResult(
|
||||||
params: LoadParams<Long>,
|
params: LoadParams<Long>,
|
||||||
mangasPage: MangasPage,
|
mangasPage: MangasPage,
|
||||||
): LoadResult.Page<Long, Pair<SManga, RaisedSearchMetadata?>> {
|
): LoadResult.Page<Long, Pair<Manga, RaisedSearchMetadata?>> {
|
||||||
mangasPage as MetadataMangasPage
|
mangasPage as MetadataMangasPage
|
||||||
val metadata = mangasPage.mangasMetadata
|
val metadata = mangasPage.mangasMetadata
|
||||||
|
|
||||||
|
val manga = mangasPage.mangas.map { it.toDomainManga(source.id) }
|
||||||
|
.let { networkToLocalManga(it) }
|
||||||
|
|
||||||
return LoadResult.Page(
|
return LoadResult.Page(
|
||||||
data = mangasPage.mangas
|
data = manga
|
||||||
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
|
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
|
||||||
prevKey = null,
|
prevKey = null,
|
||||||
nextKey = mangasPage.nextKey,
|
nextKey = mangasPage.nextKey,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user