Compare commits

..

No commits in common. "master" and "preview-173" have entirely different histories.

269 changed files with 2137 additions and 5540 deletions

View File

@ -7,7 +7,7 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{xml,sq,sqm}] [*.xml]
indent_size = 4 indent_size = 4
# noinspection EditorConfigKeyCorrectness # noinspection EditorConfigKeyCorrectness

View File

@ -14,14 +14,8 @@ jobs:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up JDK - name: Validate Gradle Wrapper
uses: actions/setup-java@v4 uses: gradle/actions/wrapper-validation@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag - name: Create Tag
run: | run: |
@ -34,6 +28,3 @@ jobs:
-H 'Accept: application/vnd.github.everest-preview+json' \ -H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \ -u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' --data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest

View File

@ -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.1 uses: tachiyomiorg/issue-moderator-action@v2.6.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
duplicate-label: Duplicate duplicate-label: Duplicate

3
.gitignore vendored
View File

@ -19,6 +19,3 @@ local.properties
google-services.json google-services.json
/app/src/main/assets/client_secrets.json /app/src/main/assets/client_secrets.json
*.apk *.apk
# Custom ignores
/keys

View File

@ -31,12 +31,12 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 75 versionCode = 73
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(useLastCommitTime = false)}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
ndk { ndk {
@ -71,8 +71,6 @@ 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"))
@ -239,7 +237,7 @@ dependencies {
implementation(libs.preferencektx) implementation(libs.preferencektx)
// Dependency injection // Dependency injection
implementation(libs.injekt) implementation(libs.injekt.core)
// Image loading // Image loading
implementation(platform(libs.coil.bom)) implementation(platform(libs.coil.bom))
@ -257,7 +255,7 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.richeditor.compose) implementation(libs.bundles.richtext)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion) implementation(libs.compose.materialmotion)
@ -265,7 +263,6 @@ 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)
@ -316,6 +313,14 @@ 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

View File

@ -82,7 +82,6 @@ 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
@ -129,7 +128,6 @@ 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()) }

View File

@ -28,6 +28,7 @@ import tachiyomi.data.source.SavedSearchRepositoryImpl
import tachiyomi.domain.chapter.interactor.DeleteChapters import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.GetChapterByUrl import tachiyomi.domain.chapter.interactor.GetChapterByUrl
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistoryByMangaId
import tachiyomi.domain.manga.interactor.DeleteByMergeId import tachiyomi.domain.manga.interactor.DeleteByMergeId
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
import tachiyomi.domain.manga.interactor.DeleteMangaById import tachiyomi.domain.manga.interactor.DeleteMangaById
@ -87,6 +88,7 @@ class SYDomainModule : InjektModule {
addFactory { DeleteChapters(get()) } addFactory { DeleteChapters(get()) }
addFactory { DeleteMangaById(get()) } addFactory { DeleteMangaById(get()) }
addFactory { FilterSerializer() } addFactory { FilterSerializer() }
addFactory { GetHistoryByMangaId(get()) }
addFactory { GetChapterByUrl(get()) } addFactory { GetChapterByUrl(get()) }
addFactory { GetSourceCategories(get()) } addFactory { GetSourceCategories(get()) }
addFactory { CreateSourceCategory(get()) } addFactory { CreateSourceCategory(get()) }

View File

@ -23,7 +23,7 @@ class GetPagePreviews(
return try { return try {
val pagePreviews = try { val pagePreviews = try {
pagePreviewCache.getPageListFromCache(manga, chapterIds, page) pagePreviewCache.getPageListFromCache(manga, chapterIds, page)
} catch (_: Exception) { } catch (e: Exception) {
source.getPagePreviewList(manga.toSManga(), chapters.map { it.toSChapter() }, page).also { source.getPagePreviewList(manga.toSManga(), chapters.map { it.toSChapter() }, page).also {
pagePreviewCache.putPageListToCache(manga, chapterIds, it) pagePreviewCache.putPageListToCache(manga, chapterIds, it)
} }

View File

@ -4,7 +4,6 @@ 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
@ -33,8 +32,9 @@ class UpdateManga(
remoteManga: SManga, remoteManga: SManga,
manualFetch: Boolean, manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(), coverCache: CoverCache = Injekt.get(),
libraryPreferences: LibraryPreferences = Injekt.get(), // SY -->
downloadManager: DownloadManager = Injekt.get(), downloadManager: DownloadManager = Injekt.get(),
// SY <--
): Boolean { ): Boolean {
val remoteTitle = try { val remoteTitle = try {
remoteManga.title remoteManga.title
@ -42,13 +42,14 @@ class UpdateManga(
"" ""
} }
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db // SY -->
val title = val title = if (remoteTitle.isNotBlank() && localManga.ogTitle != remoteTitle) {
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) { downloadManager.renameMangaDir(localManga.ogTitle, remoteTitle, localManga.source)
remoteTitle remoteTitle
} else { } else {
null null
} }
// SY <--
val coverLastModified = val coverLastModified =
when { when {
@ -68,7 +69,7 @@ class UpdateManga(
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
val success = mangaRepository.update( return mangaRepository.update(
MangaUpdate( MangaUpdate(
id = localManga.id, id = localManga.id,
title = title, title = title,
@ -83,10 +84,6 @@ class UpdateManga(
initialized = true, initialized = true,
), ),
) )
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
} }
suspend fun awaitUpdateFetchInterval( suspend fun awaitUpdateFetchInterval(

View File

@ -38,14 +38,12 @@ fun Manga.chaptersFiltered(): Boolean {
fun Manga.toSManga(): SManga = SManga.create().also { fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url it.url = url
// SY --> it.title = title
it.title = ogTitle it.artist = artist
it.artist = ogArtist it.author = author
it.author = ogAuthor it.description = description
it.description = ogDescription it.genre = genre.orEmpty().joinToString()
it.genre = ogGenre.orEmpty().joinToString() it.status = status.toInt()
it.status = ogStatus.toInt()
// SY <--
it.thumbnail_url = thumbnailUrl it.thumbnail_url = thumbnailUrl
it.initialized = initialized it.initialized = initialized
} }
@ -78,6 +76,24 @@ 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()
} }

View File

@ -88,32 +88,5 @@ class SourcePreferences(
BANDWIDTH_HERO, BANDWIDTH_HERO,
WSRV_NL, WSRV_NL,
} }
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)
fun skipPreMigration() = preferenceStore.getBoolean(Preference.appStateKey("skip_pre_migration"), false)
fun hideNotFoundMigration() = preferenceStore.getBoolean("hide_not_found_migration", false)
fun showOnlyUpdatesMigration() = preferenceStore.getBoolean("show_only_updates_migration", false)
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
fun mangadexSyncToLibraryIndexes() = preferenceStore.getStringSet(
"pref_mangadex_sync_to_library_indexes",
emptySet(),
)
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
// SY <-- // SY <--
} }

View File

@ -1,6 +1,8 @@
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?) {
@ -9,7 +11,9 @@ 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),

View File

@ -82,18 +82,10 @@ fun BrowseSourceContent(
} }
} }
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
LoadingScreen(Modifier.padding(contentPadding))
return
}
if (mangaList.itemCount == 0) {
EmptyScreen( EmptyScreen(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = when (errorState) { message = getErrorMessage(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(
@ -112,7 +104,7 @@ fun BrowseSourceContent(
// SY --> // SY -->
if (onWebViewClick != null) { if (onWebViewClick != null) {
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.action_open_in_web_view, MR.strings.action_open_in_web_view,
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
onClick = onWebViewClick, onClick = onWebViewClick,
) )
@ -121,7 +113,7 @@ fun BrowseSourceContent(
}, },
if (onHelpClick != null) { if (onHelpClick != null) {
EmptyScreenAction( EmptyScreenAction(
stringRes = MR.strings.label_help, MR.strings.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
) )
@ -136,6 +128,13 @@ 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(

View File

@ -1,9 +1,10 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.compose.animation.SizeTransform import androidx.activity.compose.BackHandler
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
@ -27,14 +28,20 @@ fun NavigatorAdaptiveSheet(
screen = screen, screen = screen,
content = { sheetNavigator -> content = { sheetNavigator ->
AdaptiveSheet( AdaptiveSheet(
onDismissRequest = onDismissRequest,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator), enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest,
) { ) {
ScreenTransition( ScreenTransition(
navigator = sheetNavigator, navigator = sheetNavigator,
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) }, transition = {
exitTransition = { fadeOut(animationSpec = tween(90)) }, fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
sizeTransform = { SizeTransform() }, fadeOut(animationSpec = tween(90))
},
)
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
) )
} }
@ -72,10 +79,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()
} }

View File

@ -36,7 +36,6 @@ 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
@ -202,7 +201,6 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = it.onClick, onClick = it.onClick,
@ -227,7 +225,6 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { showMenu = !showMenu }, onClick = { showMenu = !showMenu },
@ -292,7 +289,6 @@ fun SearchToolbar(
onSearch(searchQuery) onSearch(searchQuery)
focusManager.clearFocus() focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
focusManager.moveFocus(FocusDirection.Next)
} }
BasicTextField( BasicTextField(
@ -356,7 +352,6 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
@ -376,7 +371,6 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { onClick = {

View File

@ -1,95 +1,44 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.background import androidx.compose.foundation.clickable
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.size import androidx.compose.foundation.layout.sizeIn
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.AttachMoney import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.SwapVert
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.util.fastMaxOfOrNull import androidx.compose.ui.unit.sp
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: (manga: Manga) -> Unit, onOpenManga: () -> Unit,
onMigrate: (manga: Manga) -> Unit, onMigrate: () -> 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,
@ -97,45 +46,45 @@ fun DuplicateMangaDialog(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical) .padding(
.verticalScroll(rememberScrollState()) vertical = TabbedDialogPaddings.Vertical,
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(), .fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
Text( Text(
text = stringResource(MR.strings.possible_duplicates_title), modifier = Modifier.padding(TitlePadding),
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.possible_duplicates_summary), text = stringResource(MR.strings.confirm_add_duplicate_manga),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
) )
LazyRow( Spacer(Modifier.height(PaddingSize))
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)), TextPreferenceWidget(
contentPadding = horizontalPadding, title = stringResource(MR.strings.action_show_manga),
) { icon = Icons.Outlined.Book,
items( onPreferenceClick = {
items = duplicates, onDismissRequest()
key = { it.manga.id }, onOpenManga()
) { },
DuplicateMangaListItem( )
duplicate = it,
getSource = { sourceManager.getOrStub(it.manga.source) }, HorizontalDivider()
onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest, TextPreferenceWidget(
onOpenManga = { onOpenManga(it.manga) }, title = stringResource(MR.strings.action_migrate_duplicate),
) icon = Icons.Outlined.SwapVert,
} onPreferenceClick = {
} onDismissRequest()
onMigrate()
},
)
Column(modifier = horizontalPaddingModifier) {
HorizontalDivider() HorizontalDivider()
TextPreferenceWidget( TextPreferenceWidget(
@ -145,262 +94,33 @@ fun DuplicateMangaDialog(
onDismissRequest() onDismissRequest()
onConfirm() 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,
)
}
}
}
}
@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,
),
)
}
}
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
if (source is StubSource) { OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text( 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 modifier = Modifier
.secondaryItemAlpha() .padding(vertical = 8.dp),
.padding(top = MaterialTheme.padding.extraSmall), text = stringResource(MR.strings.action_cancel),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), color = MaterialTheme.colorScheme.primary,
verticalAlignment = Alignment.CenterVertically, style = MaterialTheme.typography.titleLarge,
) { fontSize = 16.sp,
Icon(
imageVector = iconImageVector,
contentDescription = null,
modifier = Modifier.size(MangaDetailsIconWidth),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
) )
} }
}
}
}
} }
@Composable private val PaddingSize = 16.dp
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() } private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() } private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
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

View File

@ -1,45 +0,0 @@
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(),
)
}
}

View File

@ -142,7 +142,6 @@ 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,
@ -202,7 +201,6 @@ 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,
@ -249,7 +247,6 @@ 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,
@ -306,7 +303,6 @@ 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,
@ -349,9 +345,13 @@ private fun MangaScreenSmallImpl(
} }
// SY <-- // SY <--
BackHandler(enabled = isAnySelected) { BackHandler(onBack = {
if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else {
navigateUp()
} }
})
Scaffold( Scaffold(
topBar = { topBar = {
@ -382,7 +382,6 @@ 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 },
@ -520,10 +519,8 @@ 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) {
@ -629,7 +626,6 @@ 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,
@ -676,9 +672,13 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
BackHandler(enabled = isAnySelected) { BackHandler(onBack = {
if (isAnySelected) {
onAllChapterSelected(false) onAllChapterSelected(false)
} else {
navigateUp()
} }
})
Scaffold( Scaffold(
topBar = { topBar = {
@ -696,7 +696,6 @@ 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 },
@ -815,10 +814,8 @@ 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) {

View File

@ -3,9 +3,6 @@ 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
@ -28,22 +25,18 @@ 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
@ -56,14 +49,11 @@ 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(
@ -162,32 +152,10 @@ 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 = {
@ -204,20 +172,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) val copy = (drawable as? BitmapDrawable)?.let {
?.bitmap val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
?.copy(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.HARDWARE Bitmap.Config.HARDWARE
} else { } else {
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
}, }
false, BitmapDrawable(
view.context.resources,
it.bitmap.copy(config, false),
) )
?.toDrawable(view.context.resources) } ?: drawable
?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
} }
.build() .build()

View File

@ -77,8 +77,6 @@ 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
@ -97,6 +95,8 @@ 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,10 +250,8 @@ 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,
@ -266,12 +264,15 @@ 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(
description = desc, expandedDescription = 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)
@ -593,26 +594,11 @@ 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(
description: String, expandedDescription: String,
notes: String, shrunkDescription: String,
expanded: Boolean, expanded: Boolean,
onEditNotesClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val animProgress by animateFloatAsState( val animProgress by animateFloatAsState(
@ -624,41 +610,26 @@ private fun MangaSummary(
contents = listOf( contents = listOf(
{ {
Text( Text(
// Shows at least 3 lines if no notes text = "\n\n", // Shows at least 3 lines
// 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,
) )
}, },
{ {
Column { Text(
MangaNotesSection( text = expandedDescription,
content = notes, style = MaterialTheme.typography.bodyMedium,
expanded = true,
onEditNotes = onEditNotesClicked,
) )
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
}, },
{ {
Column {
MangaNotesSection(
content = notes,
expanded = expanded,
onEditNotes = onEditNotesClicked,
)
SelectionContainer { SelectionContainer {
MarkdownRender( Text(
content = description, text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(), modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
) )
} }
}
}, },
{ {
val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background) val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)

View File

@ -1,60 +0,0 @@
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,
)
}
}

View File

@ -1,90 +0,0 @@
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!",
)
}

View File

@ -1,224 +0,0 @@
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),
)
}
}

View File

@ -38,7 +38,6 @@ 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)?,
@ -148,12 +147,6 @@ fun MangaToolbar(
), ),
) )
} }
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_notes),
onClick = onClickEditNotes,
),
)
// SY --> // SY -->
if (onClickMerge != null) { if (onClickMerge != null) {
add( add(

View File

@ -1,253 +0,0 @@
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)

View File

@ -1,6 +1,5 @@
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
@ -14,10 +13,13 @@ 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 eu.kanade.presentation.manga.components.MarkdownRender import com.halilibo.richtext.markdown.Markdown
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
@ -40,15 +42,17 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now), rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate, onRejectClick = onRejectUpdate,
) { ) {
Column( RichText(
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),
),
),
) { ) {
MarkdownRender( Markdown(content = changelogInfo)
content = changelogInfo,
flavour = GFMFlavourDescriptor(),
)
TextButton( TextButton(
onClick = onOpenInBrowser, onClick = onOpenInBrowser,

View File

@ -42,9 +42,7 @@ fun OnboardingScreen(
} }
val isLastStep = currentStep == steps.lastIndex val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0) { BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
currentStep--
}
InfoScreen( InfoScreen(
icon = Icons.Outlined.RocketLaunch, icon = Icons.Outlined.RocketLaunch,

View File

@ -13,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogE import exh.log.xLogE
import exh.source.ExhPreferences
import exh.uconfig.EHConfigurator import exh.uconfig.EHConfigurator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -29,8 +29,8 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) { fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
val exhPreferences = remember { val unsortedPreferences = remember {
Injekt.get<ExhPreferences>() Injekt.get<UnsortedPreferences>()
} }
var warnDialogOpen by remember { mutableStateOf(false) } var warnDialogOpen by remember { mutableStateOf(false) }
var configureDialogOpen by remember { mutableStateOf(false) } var configureDialogOpen by remember { mutableStateOf(false) }
@ -38,7 +38,7 @@ fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
LaunchedEffect(run) { LaunchedEffect(run) {
if (run) { if (run) {
if (exhPreferences.exhShowSettingsUploadWarning().get()) { if (unsortedPreferences.exhShowSettingsUploadWarning().get()) {
warnDialogOpen = true warnDialogOpen = true
} else { } else {
configureDialogOpen = true configureDialogOpen = true
@ -57,7 +57,7 @@ fun ConfigureExhDialog(run: Boolean, onRunning: () -> Unit) {
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
exhPreferences.exhShowSettingsUploadWarning().set(false) unsortedPreferences.exhShowSettingsUploadWarning().set(false)
configureDialogOpen = true configureDialogOpen = true
warnDialogOpen = false warnDialogOpen = false
}, },

View File

@ -72,7 +72,6 @@ import exh.pref.DelegateSourcePreferences
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.ExhPreferences
import exh.util.toAnnotatedString import exh.util.toAnnotatedString
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
@ -87,8 +86,8 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
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.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
@ -115,7 +114,6 @@ 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(
@ -156,7 +154,7 @@ object SettingsAdvancedScreen : SearchableSettings {
getBackgroundActivityGroup(), getBackgroundActivityGroup(),
getDataGroup(), getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences), getNetworkGroup(networkPreferences = networkPreferences),
getLibraryGroup(libraryPreferences = libraryPreferences), getLibraryGroup(),
getReaderGroup(basePreferences = basePreferences), getReaderGroup(basePreferences = basePreferences),
getExtensionsGroup(basePreferences = basePreferences), getExtensionsGroup(basePreferences = basePreferences),
// SY --> // SY -->
@ -324,9 +322,7 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
@Composable @Composable
private fun getLibraryGroup( private fun getLibraryGroup(): Preference.PreferenceGroup {
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
@ -354,11 +350,6 @@ 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),
),
), ),
) )
} }
@ -701,14 +692,14 @@ object SettingsAdvancedScreen : SearchableSettings {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get<SourcePreferences>() } val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
val exhPreferences = remember { Injekt.get<ExhPreferences>() } val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
val delegateSourcePreferences = remember { Injekt.get<DelegateSourcePreferences>() } val delegateSourcePreferences = remember { Injekt.get<DelegateSourcePreferences>() }
val securityPreferences = remember { Injekt.get<SecurityPreferences>() } val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(SYMR.strings.developer_tools), title = stringResource(SYMR.strings.developer_tools),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.isHentaiEnabled(), preference = unsortedPreferences.isHentaiEnabled(),
title = stringResource(SYMR.strings.toggle_hentai_features), title = stringResource(SYMR.strings.toggle_hentai_features),
subtitle = stringResource(SYMR.strings.toggle_hentai_features_summary), subtitle = stringResource(SYMR.strings.toggle_hentai_features_summary),
onValueChanged = { onValueChanged = {
@ -733,7 +724,7 @@ object SettingsAdvancedScreen : SearchableSettings {
), ),
), ),
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = exhPreferences.logLevel(), preference = unsortedPreferences.logLevel(),
title = stringResource(SYMR.strings.log_level), title = stringResource(SYMR.strings.log_level),
subtitle = stringResource(SYMR.strings.log_level_summary), subtitle = stringResource(SYMR.strings.log_level_summary),
entries = EHLogLevel.entries.mapIndexed { index, ehLogLevel -> entries = EHLogLevel.entries.mapIndexed { index, ehLogLevel ->

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
@ -48,6 +49,7 @@ object SettingsBrowseScreen : SearchableSettings {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val hideFeedTab by remember { Injekt.get<UiPreferences>().hideFeedTab().asState(scope) } val hideFeedTab by remember { Injekt.get<UiPreferences>().hideFeedTab().asState(scope) }
val uiPreferences = remember { Injekt.get<UiPreferences>() } val uiPreferences = remember { Injekt.get<UiPreferences>() }
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
// SY <-- // SY <--
return listOf( return listOf(
// SY --> // SY -->
@ -75,7 +77,7 @@ object SettingsBrowseScreen : SearchableSettings {
subtitle = stringResource(SYMR.strings.pref_source_navigation_summery), subtitle = stringResource(SYMR.strings.pref_source_navigation_summery),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.allowLocalSourceHiddenFolders(), preference = unsortedPreferences.allowLocalSourceHiddenFolders(),
title = stringResource(SYMR.strings.pref_local_source_hidden_folders), title = stringResource(SYMR.strings.pref_local_source_hidden_folders),
subtitle = stringResource(SYMR.strings.pref_local_source_hidden_folders_summery), subtitle = stringResource(SYMR.strings.pref_local_source_hidden_folders_summery),
), ),
@ -129,24 +131,6 @@ object SettingsBrowseScreen : SearchableSettings {
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)),
), ),
), ),
getMigrationCategory(sourcePreferences),
)
}
@Composable
fun getMigrationCategory(sourcePreferences: SourcePreferences): Preference.PreferenceGroup {
val skipPreMigration by sourcePreferences.skipPreMigration().collectAsState()
val migrationSources by sourcePreferences.migrationSources().collectAsState()
return Preference.PreferenceGroup(
stringResource(SYMR.strings.migration),
enabled = skipPreMigration || migrationSources.isNotEmpty(),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.skipPreMigration(),
title = stringResource(SYMR.strings.skip_pre_migration),
subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary),
),
),
) )
} }
} }

View File

@ -51,7 +51,6 @@ import exh.eh.EHentaiUpdateWorker
import exh.eh.EHentaiUpdateWorkerConstants import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.EHentaiUpdaterStats import exh.eh.EHentaiUpdaterStats
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.source.ExhPreferences
import exh.ui.login.EhLoginActivity import exh.ui.login.EhLoginActivity
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -64,6 +63,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
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.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
@ -88,22 +88,22 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
override fun getTitleRes() = SYMR.strings.pref_category_eh override fun getTitleRes() = SYMR.strings.pref_category_eh
override fun isEnabled(): Boolean = Injekt.get<ExhPreferences>().isHentaiEnabled().get() override fun isEnabled(): Boolean = Injekt.get<UnsortedPreferences>().isHentaiEnabled().get()
@Composable @Composable
fun Reconfigure( fun Reconfigure(
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
openWarnConfigureDialogController: () -> Unit, openWarnConfigureDialogController: () -> Unit,
) { ) {
var initialLoadGuard by remember { mutableStateOf(false) } var initialLoadGuard by remember { mutableStateOf(false) }
val useHentaiAtHome by exhPreferences.useHentaiAtHome().collectAsState() val useHentaiAtHome by unsortedPreferences.useHentaiAtHome().collectAsState()
val useJapaneseTitle by exhPreferences.useJapaneseTitle().collectAsState() val useJapaneseTitle by unsortedPreferences.useJapaneseTitle().collectAsState()
val useOriginalImages by exhPreferences.exhUseOriginalImages().collectAsState() val useOriginalImages by unsortedPreferences.exhUseOriginalImages().collectAsState()
val ehTagFilterValue by exhPreferences.ehTagFilterValue().collectAsState() val ehTagFilterValue by unsortedPreferences.ehTagFilterValue().collectAsState()
val ehTagWatchingValue by exhPreferences.ehTagWatchingValue().collectAsState() val ehTagWatchingValue by unsortedPreferences.ehTagWatchingValue().collectAsState()
val settingsLanguages by exhPreferences.exhSettingsLanguages().collectAsState() val settingsLanguages by unsortedPreferences.exhSettingsLanguages().collectAsState()
val enabledCategories by exhPreferences.exhEnabledCategories().collectAsState() val enabledCategories by unsortedPreferences.exhEnabledCategories().collectAsState()
val imageQuality by exhPreferences.imageQuality().collectAsState() val imageQuality by unsortedPreferences.imageQuality().collectAsState()
DisposableEffect( DisposableEffect(
useHentaiAtHome, useHentaiAtHome,
useJapaneseTitle, useJapaneseTitle,
@ -124,15 +124,15 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val exhPreferences: ExhPreferences = remember { Injekt.get() } val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
val getFlatMetadataById: GetFlatMetadataById = remember { Injekt.get() } val getFlatMetadataById: GetFlatMetadataById = remember { Injekt.get() }
val deleteFavoriteEntries: DeleteFavoriteEntries = remember { Injekt.get() } val deleteFavoriteEntries: DeleteFavoriteEntries = remember { Injekt.get() }
val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata = remember { Injekt.get() } val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata = remember { Injekt.get() }
val exhentaiEnabled by exhPreferences.enableExhentai().collectAsState() val exhentaiEnabled by unsortedPreferences.enableExhentai().collectAsState()
var runConfigureDialog by remember { mutableStateOf(false) } var runConfigureDialog by remember { mutableStateOf(false) }
val openWarnConfigureDialogController = { runConfigureDialog = true } val openWarnConfigureDialogController = { runConfigureDialog = true }
Reconfigure(exhPreferences, openWarnConfigureDialogController) Reconfigure(unsortedPreferences, openWarnConfigureDialogController)
ConfigureExhDialog(run = runConfigureDialog, onRunning = { runConfigureDialog = false }) ConfigureExhDialog(run = runConfigureDialog, onRunning = { runConfigureDialog = false })
@ -140,36 +140,36 @@ object SettingsEhScreen : SearchableSettings {
Preference.PreferenceGroup( Preference.PreferenceGroup(
stringResource(SYMR.strings.ehentai_prefs_account_settings), stringResource(SYMR.strings.ehentai_prefs_account_settings),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
getLoginPreference(exhPreferences, openWarnConfigureDialogController), getLoginPreference(unsortedPreferences, openWarnConfigureDialogController),
useHentaiAtHome(exhentaiEnabled, exhPreferences), useHentaiAtHome(exhentaiEnabled, unsortedPreferences),
useJapaneseTitle(exhentaiEnabled, exhPreferences), useJapaneseTitle(exhentaiEnabled, unsortedPreferences),
useOriginalImages(exhentaiEnabled, exhPreferences), useOriginalImages(exhentaiEnabled, unsortedPreferences),
watchedTags(exhentaiEnabled), watchedTags(exhentaiEnabled),
tagFilterThreshold(exhentaiEnabled, exhPreferences), tagFilterThreshold(exhentaiEnabled, unsortedPreferences),
tagWatchingThreshold(exhentaiEnabled, exhPreferences), tagWatchingThreshold(exhentaiEnabled, unsortedPreferences),
settingsLanguages(exhentaiEnabled, exhPreferences), settingsLanguages(exhentaiEnabled, unsortedPreferences),
enabledCategories(exhentaiEnabled, exhPreferences), enabledCategories(exhentaiEnabled, unsortedPreferences),
watchedListDefaultState(exhentaiEnabled, exhPreferences), watchedListDefaultState(exhentaiEnabled, unsortedPreferences),
imageQuality(exhentaiEnabled, exhPreferences), imageQuality(exhentaiEnabled, unsortedPreferences),
enhancedEhentaiView(exhPreferences), enhancedEhentaiView(unsortedPreferences),
), ),
), ),
Preference.PreferenceGroup( Preference.PreferenceGroup(
stringResource(SYMR.strings.favorites_sync), stringResource(SYMR.strings.favorites_sync),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
readOnlySync(exhPreferences), readOnlySync(unsortedPreferences),
syncFavoriteNotes(), syncFavoriteNotes(),
lenientSync(exhPreferences), lenientSync(unsortedPreferences),
forceSyncReset(deleteFavoriteEntries), forceSyncReset(deleteFavoriteEntries),
), ),
), ),
Preference.PreferenceGroup( Preference.PreferenceGroup(
stringResource(SYMR.strings.gallery_update_checker), stringResource(SYMR.strings.gallery_update_checker),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
updateCheckerFrequency(exhPreferences), updateCheckerFrequency(unsortedPreferences),
autoUpdateRequirements(exhPreferences), autoUpdateRequirements(unsortedPreferences),
updaterStatistics( updaterStatistics(
exhPreferences, unsortedPreferences,
getExhFavoriteMangaWithMetadata, getExhFavoriteMangaWithMetadata,
getFlatMetadataById, getFlatMetadataById,
), ),
@ -180,7 +180,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun getLoginPreference( fun getLoginPreference(
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
openWarnConfigureDialogController: () -> Unit, openWarnConfigureDialogController: () -> Unit,
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
val activityResultContract = val activityResultContract =
@ -191,9 +191,9 @@ object SettingsEhScreen : SearchableSettings {
} }
} }
val context = LocalContext.current val context = LocalContext.current
val value by exhPreferences.enableExhentai().collectAsState() val value by unsortedPreferences.enableExhentai().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.enableExhentai(), preference = unsortedPreferences.enableExhentai(),
title = stringResource(SYMR.strings.enable_exhentai), title = stringResource(SYMR.strings.enable_exhentai),
subtitle = if (!value) { subtitle = if (!value) {
stringResource(SYMR.strings.requires_login) stringResource(SYMR.strings.requires_login)
@ -202,7 +202,7 @@ object SettingsEhScreen : SearchableSettings {
}, },
onValueChanged = { newVal -> onValueChanged = { newVal ->
if (!newVal) { if (!newVal) {
exhPreferences.enableExhentai().set(false) unsortedPreferences.enableExhentai().set(false)
true true
} else { } else {
activityResultContract.launch(EhLoginActivity.newIntent(context)) activityResultContract.launch(EhLoginActivity.newIntent(context))
@ -215,10 +215,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun useHentaiAtHome( fun useHentaiAtHome(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.ListPreference<Int> { ): Preference.PreferenceItem.ListPreference<Int> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
preference = exhPreferences.useHentaiAtHome(), preference = unsortedPreferences.useHentaiAtHome(),
title = stringResource(SYMR.strings.use_hentai_at_home), title = stringResource(SYMR.strings.use_hentai_at_home),
subtitle = stringResource(SYMR.strings.use_hentai_at_home_summary), subtitle = stringResource(SYMR.strings.use_hentai_at_home_summary),
entries = persistentMapOf( entries = persistentMapOf(
@ -232,11 +232,11 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun useJapaneseTitle( fun useJapaneseTitle(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
val value by exhPreferences.useJapaneseTitle().collectAsState() val value by unsortedPreferences.useJapaneseTitle().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.useJapaneseTitle(), preference = unsortedPreferences.useJapaneseTitle(),
title = stringResource(SYMR.strings.show_japanese_titles), title = stringResource(SYMR.strings.show_japanese_titles),
subtitle = if (value) { subtitle = if (value) {
stringResource(SYMR.strings.show_japanese_titles_option_1) stringResource(SYMR.strings.show_japanese_titles_option_1)
@ -250,11 +250,11 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun useOriginalImages( fun useOriginalImages(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
val value by exhPreferences.exhUseOriginalImages().collectAsState() val value by unsortedPreferences.exhUseOriginalImages().collectAsState()
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.exhUseOriginalImages(), preference = unsortedPreferences.exhUseOriginalImages(),
title = stringResource(SYMR.strings.use_original_images), title = stringResource(SYMR.strings.use_original_images),
subtitle = if (value) { subtitle = if (value) {
stringResource(SYMR.strings.use_original_images_on) stringResource(SYMR.strings.use_original_images_on)
@ -351,9 +351,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun tagFilterThreshold( fun tagFilterThreshold(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.TextPreference { ): Preference.PreferenceItem.TextPreference {
val value by exhPreferences.ehTagFilterValue().collectAsState() val value by unsortedPreferences.ehTagFilterValue().collectAsState()
var dialogOpen by remember { mutableStateOf(false) } var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) { if (dialogOpen) {
TagThresholdDialog( TagThresholdDialog(
@ -364,7 +364,7 @@ object SettingsEhScreen : SearchableSettings {
outsideRangeError = stringResource(SYMR.strings.tag_filtering_threshhold_error), outsideRangeError = stringResource(SYMR.strings.tag_filtering_threshhold_error),
onValueChange = { onValueChange = {
dialogOpen = false dialogOpen = false
exhPreferences.ehTagFilterValue().set(it) unsortedPreferences.ehTagFilterValue().set(it)
}, },
) )
} }
@ -381,9 +381,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun tagWatchingThreshold( fun tagWatchingThreshold(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.TextPreference { ): Preference.PreferenceItem.TextPreference {
val value by exhPreferences.ehTagWatchingValue().collectAsState() val value by unsortedPreferences.ehTagWatchingValue().collectAsState()
var dialogOpen by remember { mutableStateOf(false) } var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) { if (dialogOpen) {
TagThresholdDialog( TagThresholdDialog(
@ -394,7 +394,7 @@ object SettingsEhScreen : SearchableSettings {
outsideRangeError = stringResource(SYMR.strings.tag_watching_threshhold_error), outsideRangeError = stringResource(SYMR.strings.tag_watching_threshhold_error),
onValueChange = { onValueChange = {
dialogOpen = false dialogOpen = false
exhPreferences.ehTagWatchingValue().set(it) unsortedPreferences.ehTagWatchingValue().set(it)
}, },
) )
} }
@ -604,9 +604,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun settingsLanguages( fun settingsLanguages(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.TextPreference { ): Preference.PreferenceItem.TextPreference {
val value by exhPreferences.exhSettingsLanguages().collectAsState() val value by unsortedPreferences.exhSettingsLanguages().collectAsState()
var dialogOpen by remember { mutableStateOf(false) } var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) { if (dialogOpen) {
LanguagesDialog( LanguagesDialog(
@ -614,7 +614,7 @@ object SettingsEhScreen : SearchableSettings {
initialValue = value, initialValue = value,
onValueChange = { onValueChange = {
dialogOpen = false dialogOpen = false
exhPreferences.exhSettingsLanguages().set(it) unsortedPreferences.exhSettingsLanguages().set(it)
}, },
) )
} }
@ -770,9 +770,9 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun enabledCategories( fun enabledCategories(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.TextPreference { ): Preference.PreferenceItem.TextPreference {
val value by exhPreferences.exhEnabledCategories().collectAsState() val value by unsortedPreferences.exhEnabledCategories().collectAsState()
var dialogOpen by remember { mutableStateOf(false) } var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) { if (dialogOpen) {
FrontPageCategoriesDialog( FrontPageCategoriesDialog(
@ -780,7 +780,7 @@ object SettingsEhScreen : SearchableSettings {
initialValue = value, initialValue = value,
onValueChange = { onValueChange = {
dialogOpen = false dialogOpen = false
exhPreferences.exhEnabledCategories().set(it) unsortedPreferences.exhEnabledCategories().set(it)
}, },
) )
} }
@ -797,10 +797,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun watchedListDefaultState( fun watchedListDefaultState(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.SwitchPreference { ): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.exhWatchedListDefaultState(), preference = unsortedPreferences.exhWatchedListDefaultState(),
title = stringResource(SYMR.strings.watched_list_default), title = stringResource(SYMR.strings.watched_list_default),
subtitle = stringResource(SYMR.strings.watched_list_state_summary), subtitle = stringResource(SYMR.strings.watched_list_state_summary),
enabled = exhentaiEnabled, enabled = exhentaiEnabled,
@ -810,10 +810,10 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun imageQuality( fun imageQuality(
exhentaiEnabled: Boolean, exhentaiEnabled: Boolean,
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.ListPreference<String> { ): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
preference = exhPreferences.imageQuality(), preference = unsortedPreferences.imageQuality(),
title = stringResource(SYMR.strings.eh_image_quality_summary), title = stringResource(SYMR.strings.eh_image_quality_summary),
subtitle = stringResource(SYMR.strings.eh_image_quality), subtitle = stringResource(SYMR.strings.eh_image_quality),
entries = persistentMapOf( entries = persistentMapOf(
@ -829,18 +829,18 @@ object SettingsEhScreen : SearchableSettings {
} }
@Composable @Composable
fun enhancedEhentaiView(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference { fun enhancedEhentaiView(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.enhancedEHentaiView(), preference = unsortedPreferences.enhancedEHentaiView(),
title = stringResource(SYMR.strings.pref_enhanced_e_hentai_view), title = stringResource(SYMR.strings.pref_enhanced_e_hentai_view),
subtitle = stringResource(SYMR.strings.pref_enhanced_e_hentai_view_summary), subtitle = stringResource(SYMR.strings.pref_enhanced_e_hentai_view_summary),
) )
} }
@Composable @Composable
fun readOnlySync(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference { fun readOnlySync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.exhReadOnlySync(), preference = unsortedPreferences.exhReadOnlySync(),
title = stringResource(SYMR.strings.disable_favorites_uploading), title = stringResource(SYMR.strings.disable_favorites_uploading),
subtitle = stringResource(SYMR.strings.disable_favorites_uploading_summary), subtitle = stringResource(SYMR.strings.disable_favorites_uploading_summary),
) )
@ -863,9 +863,9 @@ object SettingsEhScreen : SearchableSettings {
} }
@Composable @Composable
fun lenientSync(exhPreferences: ExhPreferences): Preference.PreferenceItem.SwitchPreference { fun lenientSync(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.SwitchPreference {
return Preference.PreferenceItem.SwitchPreference( return Preference.PreferenceItem.SwitchPreference(
preference = exhPreferences.exhLenientSync(), preference = unsortedPreferences.exhLenientSync(),
title = stringResource(SYMR.strings.ignore_sync_errors), title = stringResource(SYMR.strings.ignore_sync_errors),
subtitle = stringResource(SYMR.strings.ignore_sync_errors_summary), subtitle = stringResource(SYMR.strings.ignore_sync_errors_summary),
) )
@ -935,12 +935,12 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun updateCheckerFrequency( fun updateCheckerFrequency(
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.ListPreference<Int> { ): Preference.PreferenceItem.ListPreference<Int> {
val value by exhPreferences.exhAutoUpdateFrequency().collectAsState() val value by unsortedPreferences.exhAutoUpdateFrequency().collectAsState()
val context = LocalContext.current val context = LocalContext.current
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
preference = exhPreferences.exhAutoUpdateFrequency(), preference = unsortedPreferences.exhAutoUpdateFrequency(),
title = stringResource(SYMR.strings.time_between_batches), title = stringResource(SYMR.strings.time_between_batches),
subtitle = if (value == 0) { subtitle = if (value == 0) {
stringResource(SYMR.strings.time_between_batches_summary_1, stringResource(MR.strings.app_name)) stringResource(SYMR.strings.time_between_batches_summary_1, stringResource(MR.strings.app_name))
@ -971,12 +971,12 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun autoUpdateRequirements( fun autoUpdateRequirements(
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
): Preference.PreferenceItem.MultiSelectListPreference { ): Preference.PreferenceItem.MultiSelectListPreference {
val value by exhPreferences.exhAutoUpdateRequirements().collectAsState() val value by unsortedPreferences.exhAutoUpdateRequirements().collectAsState()
val context = LocalContext.current val context = LocalContext.current
return Preference.PreferenceItem.MultiSelectListPreference( return Preference.PreferenceItem.MultiSelectListPreference(
preference = exhPreferences.exhAutoUpdateRequirements(), preference = unsortedPreferences.exhAutoUpdateRequirements(),
title = stringResource(SYMR.strings.auto_update_restrictions), title = stringResource(SYMR.strings.auto_update_restrictions),
subtitle = remember(value) { subtitle = remember(value) {
context.stringResource( context.stringResource(
@ -1139,7 +1139,7 @@ object SettingsEhScreen : SearchableSettings {
@Composable @Composable
fun updaterStatistics( fun updaterStatistics(
exhPreferences: ExhPreferences, unsortedPreferences: UnsortedPreferences,
getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata, getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata,
getFlatMetadataById: GetFlatMetadataById, getFlatMetadataById: GetFlatMetadataById,
): Preference.PreferenceItem.TextPreference { ): Preference.PreferenceItem.TextPreference {
@ -1150,7 +1150,7 @@ object SettingsEhScreen : SearchableSettings {
value = withIOContext { value = withIOContext {
try { try {
val stats = val stats =
exhPreferences.exhAutoUpdateStats().get().nullIfBlank()?.let { unsortedPreferences.exhAutoUpdateStats().get().nullIfBlank()?.let {
Json.decodeFromString<EHentaiUpdaterStats>(it) Json.decodeFromString<EHentaiUpdaterStats>(it)
} }

View File

@ -25,6 +25,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -58,6 +59,9 @@ object SettingsLibraryScreen : SearchableSettings {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
// SY -->
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
// SY <--
return listOf( return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
@ -65,6 +69,7 @@ object SettingsLibraryScreen : SearchableSettings {
getBehaviorGroup(libraryPreferences), getBehaviorGroup(libraryPreferences),
// SY --> // SY -->
getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences), getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences),
getMigrationCategory(unsortedPreferences),
// SY <-- // SY <--
) )
} }
@ -295,5 +300,22 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
) )
} }
@Composable
fun getMigrationCategory(unsortedPreferences: UnsortedPreferences): Preference.PreferenceGroup {
val skipPreMigration by unsortedPreferences.skipPreMigration().collectAsState()
val migrationSources by unsortedPreferences.migrationSources().collectAsState()
return Preference.PreferenceGroup(
stringResource(SYMR.strings.migration),
enabled = skipPreMigration || migrationSources.isNotEmpty(),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
preference = unsortedPreferences.skipPreMigration(),
title = stringResource(SYMR.strings.skip_pre_migration),
subtitle = stringResource(SYMR.strings.pref_skip_pre_migration_summary),
),
),
)
}
// SY <-- // SY <--
} }

View File

@ -44,6 +44,7 @@ import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@ -64,13 +65,14 @@ object SettingsMangadexScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val sourcePreferences: SourcePreferences = remember { Injekt.get() } val sourcePreferences: SourcePreferences = remember { Injekt.get() }
val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
val trackPreferences: TrackPreferences = remember { Injekt.get() } val trackPreferences: TrackPreferences = remember { Injekt.get() }
val mdex = remember { MdUtil.getEnabledMangaDex(sourcePreferences) } ?: return emptyList() val mdex = remember { MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences) } ?: return emptyList()
return listOf( return listOf(
loginPreference(mdex, trackPreferences), loginPreference(mdex, trackPreferences),
preferredMangaDexId(sourcePreferences), preferredMangaDexId(unsortedPreferences, sourcePreferences),
syncMangaDexIntoThis(sourcePreferences), syncMangaDexIntoThis(unsortedPreferences),
syncLibraryToMangaDex(), syncLibraryToMangaDex(),
) )
} }
@ -172,10 +174,11 @@ object SettingsMangadexScreen : SearchableSettings {
@Composable @Composable
fun preferredMangaDexId( fun preferredMangaDexId(
unsortedPreferences: UnsortedPreferences,
sourcePreferences: SourcePreferences, sourcePreferences: SourcePreferences,
): Preference.PreferenceItem.ListPreference<String> { ): Preference.PreferenceItem.ListPreference<String> {
return Preference.PreferenceItem.ListPreference( return Preference.PreferenceItem.ListPreference(
preference = sourcePreferences.preferredMangaDexId(), preference = unsortedPreferences.preferredMangaDexId(),
title = stringResource(SYMR.strings.mangadex_preffered_source), title = stringResource(SYMR.strings.mangadex_preffered_source),
subtitle = stringResource(SYMR.strings.mangadex_preffered_source_summary), subtitle = stringResource(SYMR.strings.mangadex_preffered_source_summary),
entries = MdUtil.getEnabledMangaDexs(sourcePreferences) entries = MdUtil.getEnabledMangaDexs(sourcePreferences)
@ -247,7 +250,7 @@ object SettingsMangadexScreen : SearchableSettings {
} }
@Composable @Composable
fun syncMangaDexIntoThis(sourcePreferences: SourcePreferences): Preference.PreferenceItem.TextPreference { fun syncMangaDexIntoThis(unsortedPreferences: UnsortedPreferences): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current val context = LocalContext.current
var dialogOpen by remember { mutableStateOf(false) } var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) { if (dialogOpen) {
@ -255,7 +258,7 @@ object SettingsMangadexScreen : SearchableSettings {
onDismissRequest = { dialogOpen = false }, onDismissRequest = { dialogOpen = false },
onSelectionConfirmed = { items -> onSelectionConfirmed = { items ->
dialogOpen = false dialogOpen = false
sourcePreferences.mangadexSyncToLibraryIndexes().set( unsortedPreferences.mangadexSyncToLibraryIndexes().set(
List(items.size) { index -> (index + 1).toString() }.toSet(), List(items.size) { index -> (index + 1).toString() }.toSet(),
) )
LibraryUpdateJob.startNow( LibraryUpdateJob.startNow(

View File

@ -227,6 +227,13 @@ object SettingsReaderScreen : SearchableSettings {
preference = readerPreferences.skipDupe(), preference = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters), title = stringResource(MR.strings.pref_skip_dupe_chapters),
), ),
// SY -->
Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.markReadDupe(),
title = stringResource(SYMR.strings.pref_mark_read_dupe_chapters),
subtitle = stringResource(SYMR.strings.pref_mark_read_dupe_chapters_summary),
),
// SY <--
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.alwaysShowChapterTransition(), preference = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition), title = stringResource(MR.strings.pref_always_show_chapter_transition),

View File

@ -30,11 +30,8 @@ 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
@ -231,9 +228,7 @@ object SettingsTrackingScreen : SearchableSettings {
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.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)) },
@ -244,9 +239,7 @@ object SettingsTrackingScreen : SearchableSettings {
var hidePassword by remember { mutableStateOf(true) } var hidePassword by remember { mutableStateOf(true) }
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.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)) },
@ -295,7 +288,7 @@ object SettingsTrackingScreen : SearchableSettings {
} }
}, },
) { ) {
val id = if (processing) MR.strings.logging_in else MR.strings.login val id = if (processing) MR.strings.loading else MR.strings.login
Text(text = stringResource(id)) Text(text = stringResource(id))
} }
}, },

View File

@ -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.util.htmlReadyLicenseContent import com.mikepenz.aboutlibraries.ui.compose.m3.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

View File

@ -1,10 +1,8 @@
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
@ -14,7 +12,6 @@ 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
@ -45,16 +42,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
@ -76,45 +73,18 @@ 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)
@ -129,6 +99,20 @@ 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 <--
},
) )
} }
@ -240,9 +224,15 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
} }
} }
suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext { suspend fun removeMangaBySourceId(/* SY --> */keepReadManga: Boolean /* SY <-- */) = withNonCancellableContext {
val state = state.value as? State.Ready ?: return@withNonCancellableContext val state = state.value as? State.Ready ?: return@withNonCancellableContext
database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong()) // SY -->
if (keepReadManga) {
database.mangasQueries.deleteMangasNotInLibraryAndNotReadBySourceIds(state.selection)
} else {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
}
// SY <--
database.historyQueries.removeResettedHistory() database.historyQueries.removeResettedHistory()
} }

View File

@ -25,6 +25,7 @@ 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
@ -85,8 +86,7 @@ internal fun BasePreferenceWidget(
} }
} }
@Composable internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
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 {
}, },
label = "highlight", label = "highlight",
) )
return this.background(color = highlight) Modifier.background(color = highlight)
} }
internal val TrailingWidgetBuffer = 16.dp internal val TrailingWidgetBuffer = 16.dp

View File

@ -60,9 +60,7 @@ fun UpdateScreen(
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit, onOpenChapter: (UpdatesItem) -> Unit,
) { ) {
BackHandler(enabled = state.selectionMode) { BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
onSelectAll(false)
}
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->

View File

@ -1,46 +1,12 @@
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
@ -49,28 +15,18 @@ 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.materialSharedAxisXIn import soup.compose.material.motion.animation.materialSharedAxisX
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 {
@ -103,278 +59,39 @@ 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 screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) { val slideDistance = rememberSlideDistance()
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,
modifier = modifier, transition = {
enterTransition = { materialSharedAxisX(
if (it == SwipeEdge.Right) { forward = navigator.lastEvent != StackEvent.Pop,
materialSharedAxisXIn(forward = false, slideDistance = slideDistance) slideDistance = slideDistance,
} else { )
materialSharedAxisXIn(forward = true, slideDistance = slideDistance) },
} modifier = modifier,
},
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() },
) { ) {
val view = LocalView.current AnimatedContent(
val viewConfig = LocalViewConfiguration.current targetState = navigator.lastItem,
val scope = rememberCoroutineScope() transitionSpec = transition,
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,
transitionSpec = { label = "transition",
val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack ) { screen ->
ContentTransform( navigator.saveableState("transition", screen) {
targetContentEnter = if (pop) { content(screen)
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()) },
)
}

View File

@ -3,6 +3,7 @@ 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
@ -26,6 +27,7 @@ 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
@ -37,13 +39,19 @@ 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,
@ -57,8 +65,11 @@ 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) }
@ -113,6 +124,40 @@ 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)
}
}
} }
} }

View File

@ -328,7 +328,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
return super.generateFileName( return super.generateFileName(
logLevel, logLevel,
timestamp, timestamp,
) + "-${BuildConfig.BUILD_TYPE}.txt" ) + "-${BuildConfig.BUILD_TYPE}.log"
} }
} }
flattener { timeMillis, level, tag, message -> flattener { timeMillis, level, tag, message ->

View File

@ -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.shareBackupPendingActivity(context, file.uri), NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
) )
show(Notifications.ID_BACKUP_COMPLETE) show(Notifications.ID_BACKUP_COMPLETE)

View File

@ -118,15 +118,13 @@ class MangaBackupCreator(
private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/* SY <-- */) = private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/* SY <-- */) =
BackupManga( BackupManga(
url = this.url, url = this.url,
// SY --> title = this.title,
title = this.ogTitle, artist = this.artist,
artist = this.ogArtist, author = this.author,
author = this.ogAuthor, description = this.description,
description = this.ogDescription, genre = this.genre.orEmpty(),
genre = this.ogGenre.orEmpty(), status = this.status.toInt(),
status = this.ogStatus.toInt(), thumbnailUrl = this.thumbnailUrl,
thumbnailUrl = this.ogThumbnailUrl,
// SY <--
favorite = this.favorite, favorite = this.favorite,
source = this.source, source = this.source,
dateAdded = this.dateAdded, dateAdded = this.dateAdded,
@ -137,7 +135,6 @@ 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 {

View File

@ -38,10 +38,8 @@ 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(),
@ -79,7 +77,6 @@ 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,
) )
} }
} }

View File

@ -139,15 +139,13 @@ class MangaRestorer(
mangasQueries.update( mangasQueries.update(
source = manga.source, source = manga.source,
url = manga.url, url = manga.url,
// SY --> artist = manga.artist,
artist = manga.ogArtist, author = manga.author,
author = manga.ogAuthor, description = manga.description,
description = manga.ogDescription, genre = manga.genre?.joinToString(separator = ", "),
genre = manga.ogGenre?.joinToString(separator = ", "), title = manga.title,
title = manga.ogTitle, status = manga.status,
status = manga.ogStatus, thumbnailUrl = manga.thumbnailUrl,
thumbnailUrl = manga.ogThumbnailUrl,
// SY <--
favorite = manga.favorite, favorite = manga.favorite,
lastUpdate = manga.lastUpdate, lastUpdate = manga.lastUpdate,
nextUpdate = null, nextUpdate = null,
@ -161,7 +159,6 @@ 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
@ -277,15 +274,13 @@ class MangaRestorer(
mangasQueries.insert( mangasQueries.insert(
source = manga.source, source = manga.source,
url = manga.url, url = manga.url,
// SY --> artist = manga.artist,
artist = manga.ogArtist, author = manga.author,
author = manga.ogAuthor, description = manga.description,
description = manga.ogDescription, genre = manga.genre,
genre = manga.ogGenre, title = manga.title,
title = manga.ogTitle, status = manga.status,
status = manga.ogStatus, thumbnailUrl = manga.thumbnailUrl,
thumbnailUrl = manga.ogThumbnailUrl,
// SY <--
favorite = manga.favorite, favorite = manga.favorite,
lastUpdate = manga.lastUpdate, lastUpdate = manga.lastUpdate,
nextUpdate = 0L, nextUpdate = 0L,
@ -297,7 +292,6 @@ 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()
} }
@ -462,7 +456,6 @@ class MangaRestorer(
} }
// SY --> // SY -->
/** /**
* Restore the categories from Json * Restore the categories from Json
* *

View File

@ -307,41 +307,6 @@ 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

View File

@ -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 }
} }
} }
@ -291,7 +291,6 @@ class DownloadManager(
} }
// SY --> // SY -->
/** /**
* return the list of all manga folders * return the list of all manga folders
*/ */
@ -317,14 +316,11 @@ 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)
.getOrNull()
if (mangaFolder != null) {
cleaned += 1 + mangaFolder.listFiles().orEmpty().size cleaned += 1 + mangaFolder.listFiles().orEmpty().size
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
return cleaned return cleaned
} }
}
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
cleaned += filesWithNoChapter.size cleaned += filesWithNoChapter.size
@ -340,8 +336,8 @@ class DownloadManager(
} }
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrNull() val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
if (mangaFolder != null && !mangaFolder.listFiles().isNullOrEmpty()) { if (!mangaFolder.listFiles().isNullOrEmpty()) {
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
} else { } else {
@ -399,38 +395,6 @@ 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
* *
@ -441,10 +405,7 @@ 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).getOrElse { e -> val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
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()

View File

@ -14,7 +14,6 @@ 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.
@ -36,36 +35,20 @@ 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): Result<UniFile> { internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
val downloadsDir = downloadsDir try {
if (downloadsDir == null) { return downloadsDir!!
logcat(LogPriority.ERROR) { "Failed to create download directory" } .createDirectory(getSourceDirName(source))!!
return Result.failure( .createDirectory(getMangaDirName(mangaTitle))!!
IOException(context.stringResource(MR.strings.storage_failed_to_create_download_directory)), } catch (e: Throwable) {
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)
} }
/** /**
@ -120,7 +103,6 @@ class DownloadProvider(
} }
// SY --> // SY -->
/** /**
* Returns a list of all files in manga directory * Returns a list of all files in manga directory
* *

View File

@ -327,11 +327,7 @@ 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).getOrElse { e -> val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source)
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) {
@ -383,11 +379,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.LoadPage page.status = Page.State.LOAD_PAGE
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(e) page.status = Page.State.ERROR
} }
} }
@ -473,12 +469,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(e) page.status = Page.State.ERROR
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)
} }
} }
@ -498,7 +494,7 @@ class Downloader(
filename: String, filename: String,
dataSaver: DataSaver, dataSaver: DataSaver,
): UniFile { ): UniFile {
page.status = Page.State.DownloadImage page.status = Page.State.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return flow { return flow {
val response = source.getImage(page, dataSaver) val response = source.getImage(page, dataSaver)

View File

@ -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)

View File

@ -23,7 +23,6 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
@ -65,6 +64,7 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@ -101,12 +101,9 @@ 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 kotlin.concurrent.atomics.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.atomics.AtomicInt import java.util.concurrent.atomic.AtomicInteger
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) {
@ -346,7 +343,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 = AtomicInt(0) val progressCount = AtomicInteger(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?>>()
@ -411,7 +408,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (chaptersToDownload.isNotEmpty()) { if (chaptersToDownload.isNotEmpty()) {
downloadChapters(manga, chaptersToDownload) downloadChapters(manga, chaptersToDownload)
hasDownloads.store(true) hasDownloads.set(true)
} }
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
@ -444,7 +441,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates) notifier.showUpdateNotifications(newUpdates)
if (hasDownloads.load()) { if (hasDownloads.get()) {
downloadManager.startDownloads() downloadManager.startDownloads()
} }
} }
@ -510,7 +507,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 = AtomicInt(0) val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope { coroutineScope {
@ -558,12 +555,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
// SY --> // SY -->
/** /**
* filter all follows from Mangadex and only add reading or rereading manga to library * filter all follows from Mangadex and only add reading or rereading manga to library
*/ */
private suspend fun syncFollows() = coroutineScope { private suspend fun syncFollows() = coroutineScope {
val preferences = Injekt.get<SourcePreferences>() val preferences = Injekt.get<UnsortedPreferences>()
var count = 0 var count = 0
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager = sourceManager) val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager = sourceManager)
?: return@coroutineScope ?: return@coroutineScope
@ -586,7 +582,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( dbManga = networkToLocalManga.await(
Manga.create().copy( Manga.create().copy(
url = networkManga.url, url = networkManga.url,
ogTitle = networkManga.title, ogTitle = networkManga.title,
@ -645,7 +641,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInt, completed: AtomicInteger,
manga: Manga, manga: Manga,
block: suspend () -> Unit, block: suspend () -> Unit,
) = coroutineScope { ) = coroutineScope {
@ -654,7 +650,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
updatingManga.add(manga) updatingManga.add(manga)
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.load(), completed.get(),
mangaToUpdate.size, mangaToUpdate.size,
) )
@ -663,10 +659,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
ensureActive() ensureActive()
updatingManga.remove(manga) updatingManga.remove(manga)
completed.incrementAndFetch() completed.getAndIncrement()
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.load(), completed.get(),
mangaToUpdate.size, mangaToUpdate.size,
) )
} }
@ -735,7 +731,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private const val KEY_TARGET = "target" private const val KEY_TARGET = "target"
// SY --> // SY -->
/** /**
* Key for group to update. * Key for group to update.
*/ */

View File

@ -37,11 +37,8 @@ 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 kotlin.concurrent.atomics.AtomicInt import java.util.concurrent.atomic.AtomicInteger
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) {
@ -100,7 +97,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 = AtomicInt(0) val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope { coroutineScope {
@ -145,7 +142,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInt, completed: AtomicInteger,
manga: Manga, manga: Manga,
block: suspend () -> Unit, block: suspend () -> Unit,
) = coroutineScope { ) = coroutineScope {
@ -154,7 +151,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
updatingManga.add(manga) updatingManga.add(manga)
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.load(), completed.get(),
mangaToUpdate.size, mangaToUpdate.size,
) )
@ -163,10 +160,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
ensureActive() ensureActive()
updatingManga.remove(manga) updatingManga.remove(manga)
completed.fetchAndIncrement() completed.getAndIncrement()
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.load(), completed.get(),
mangaToUpdate.size, mangaToUpdate.size,
) )
} }

View File

@ -602,17 +602,18 @@ class NotificationReceiver : BroadcastReceiver() {
} }
/** /**
* Returns [PendingIntent] that directly launches a share activity for a backup file. * Returns [PendingIntent] that starts 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 shareBackupPendingActivity(context: Context, uri: Uri): PendingIntent { internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent {
val intent = uri.toShareIntent(context, "application/x-protobuf+gzip").apply { val intent = Intent(context, NotificationReceiver::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) action = ACTION_SHARE_BACKUP
putExtra(EXTRA_URI, uri)
} }
return PendingIntent.getActivity( return PendingIntent.getBroadcast(
context, context,
0, 0,
intent, intent,

View File

@ -108,7 +108,6 @@ class BangumiApi(
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchResult>() .parseAs<BGMSearchResult>()
.data .data
.filter { it.platform == null || it.platform == "漫画" }
.map { it.toTrackSearch(trackId) } .map { it.toTrackSearch(trackId) }
} }
} }

View File

@ -25,7 +25,6 @@ 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 <--

View File

@ -18,6 +18,8 @@ import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class MdList(id: Long) : BaseTracker(id, "MDList") { class MdList(id: Long) : BaseTracker(id, "MDList") {
@ -28,7 +30,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
.toImmutableList() .toImmutableList()
} }
private val mdex by lazy { MdUtil.getEnabledMangaDex() } private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) }
val interceptor = MangaDexAuthInterceptor(trackPreferences, this) val interceptor = MangaDexAuthInterceptor(trackPreferences, this)

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.di
import android.app.Application import android.app.Application
import exh.pref.DelegateSourcePreferences import exh.pref.DelegateSourcePreferences
import exh.source.ExhPreferences import tachiyomi.domain.UnsortedPreferences
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
class SYPreferenceModule(val application: Application) : InjektModule { class SYPreferenceModule(val application: Application) : InjektModule {
@ -15,7 +15,7 @@ class SYPreferenceModule(val application: Application) : InjektModule {
} }
addSingletonFactory { addSingletonFactory {
ExhPreferences(get()) UnsortedPreferences(get())
} }
} }
} }

View File

@ -12,18 +12,16 @@ 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 kotlin.concurrent.atomics.AtomicReference import java.util.concurrent.atomic.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() {
@ -81,7 +79,7 @@ abstract class Installer(private val service: Service) {
* @see waitingInstall * @see waitingInstall
*/ */
fun continueQueue(resultStep: InstallStep) { fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.exchange(null) val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) { if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep) extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue() checkQueue()
@ -117,10 +115,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.store(null) waitingInstall.set(null)
} }
protected fun getActiveEntry(): Entry? = waitingInstall.load() protected fun getActiveEntry(): Entry? = waitingInstall.get()
/** /**
* Cancels queue for the provided download ID if exists. * Cancels queue for the provided download ID if exists.
@ -128,13 +126,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.load() val waitingInstall = this.waitingInstall.get()
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.store(null) this.waitingInstall.set(null)
checkQueue() checkQueue()
} }
extensionManager.updateInstallStep(downloadId, InstallStep.Idle) extensionManager.updateInstallStep(downloadId, InstallStep.Idle)

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@ -20,7 +19,6 @@ import exh.source.EH_SOURCE_ID
import exh.source.EIGHTMUSES_SOURCE_ID import exh.source.EIGHTMUSES_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
import exh.source.ExhPreferences
import exh.source.HBROWSE_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.PURURIN_SOURCE_ID import exh.source.PURURIN_SOURCE_ID
@ -38,6 +36,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.StubSourceRepository import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@ -70,15 +69,14 @@ class AndroidSourceManager(
} }
// SY --> // SY -->
private val exhPreferences: ExhPreferences by injectLazy() private val preferences: UnsortedPreferences by injectLazy()
private val sourcePreferences: SourcePreferences by injectLazy()
// SY <-- // SY <--
init { init {
scope.launch { scope.launch {
extensionManager.installedExtensionsFlow extensionManager.installedExtensionsFlow
// SY --> // SY -->
.combine(exhPreferences.enableExhentai().changes()) { extensions, enableExhentai -> .combine(preferences.enableExhentai().changes()) { extensions, enableExhentai ->
extensions to enableExhentai extensions to enableExhentai
} }
// SY <-- // SY <--
@ -90,7 +88,7 @@ class AndroidSourceManager(
Injekt.get(), Injekt.get(),
Injekt.get(), Injekt.get(),
// SY --> // SY -->
sourcePreferences.allowLocalSourceHiddenFolders()::get, preferences.allowLocalSourceHiddenFolders()::get,
// SY <-- // SY <--
), ),
), ),

View File

@ -44,7 +44,6 @@ import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
import exh.metadata.metadata.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.RaisedSearchMetadata.Companion.toGenreString import exh.metadata.metadata.RaisedSearchMetadata.Companion.toGenreString
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.ExhPreferences
import exh.ui.login.EhLoginActivity import exh.ui.login.EhLoginActivity
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
@ -85,6 +84,7 @@ import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
import tachiyomi.core.common.util.lang.runAsObservable import tachiyomi.core.common.util.lang.runAsObservable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.UnsortedPreferences
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -117,7 +117,7 @@ class EHentai(
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = true
private val exhPreferences: ExhPreferences by injectLazy() private val preferences: UnsortedPreferences by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy()
/** /**
@ -476,7 +476,7 @@ class EHentai(
} }
private fun <T : MangasPage> T.checkValid(): MangasPage = private fun <T : MangasPage> T.checkValid(): MangasPage =
if (exh && mangas.isEmpty() && exhPreferences.igneousVal().get().equals("mystery", true)) { if (exh && mangas.isEmpty() && preferences.igneousVal().get().equals("mystery", true)) {
throw Exception( throw Exception(
"Invalid igneous cookie, try re-logging or finding a correct one to input in the login menu", "Invalid igneous cookie, try re-logging or finding a correct one to input in the login menu",
) )
@ -879,30 +879,30 @@ class EHentai(
} }
fun spPref() = if (exh) { fun spPref() = if (exh) {
exhPreferences.exhSettingsProfile() preferences.exhSettingsProfile()
} else { } else {
exhPreferences.ehSettingsProfile() preferences.ehSettingsProfile()
} }
private fun rawCookies(sp: Int): Map<String, String> { private fun rawCookies(sp: Int): Map<String, String> {
val cookies: MutableMap<String, String> = mutableMapOf() val cookies: MutableMap<String, String> = mutableMapOf()
if (exhPreferences.enableExhentai().get()) { if (preferences.enableExhentai().get()) {
cookies[EhLoginActivity.MEMBER_ID_COOKIE] = exhPreferences.memberIdVal().get() cookies[EhLoginActivity.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
cookies[EhLoginActivity.PASS_HASH_COOKIE] = exhPreferences.passHashVal().get() cookies[EhLoginActivity.PASS_HASH_COOKIE] = preferences.passHashVal().get()
cookies[EhLoginActivity.IGNEOUS_COOKIE] = exhPreferences.igneousVal().get() cookies[EhLoginActivity.IGNEOUS_COOKIE] = preferences.igneousVal().get()
cookies["sp"] = sp.toString() cookies["sp"] = sp.toString()
val sessionKey = exhPreferences.exhSettingsKey().get() val sessionKey = preferences.exhSettingsKey().get()
if (sessionKey.isNotBlank()) { if (sessionKey.isNotBlank()) {
cookies["sk"] = sessionKey cookies["sk"] = sessionKey
} }
val sessionCookie = exhPreferences.exhSessionCookie().get() val sessionCookie = preferences.exhSessionCookie().get()
if (sessionCookie.isNotBlank()) { if (sessionCookie.isNotBlank()) {
cookies["s"] = sessionCookie cookies["s"] = sessionCookie
} }
val hathPerksCookie = exhPreferences.exhHathPerksCookies().get() val hathPerksCookie = preferences.exhHathPerksCookies().get()
if (hathPerksCookie.isNotBlank()) { if (hathPerksCookie.isNotBlank()) {
cookies["hath_perks"] = hathPerksCookie cookies["hath_perks"] = hathPerksCookie
} }
@ -949,7 +949,7 @@ class EHentai(
ToplistOptions(), ToplistOptions(),
Filter.Separator(), Filter.Separator(),
AutoCompleteTags(), AutoCompleteTags(),
Watched(isEnabled = exhPreferences.exhWatchedListDefaultState().get()), Watched(isEnabled = preferences.exhWatchedListDefaultState().get()),
GenreGroup(), GenreGroup(),
AdvancedGroup(), AdvancedGroup(),
ReverseFilter(), ReverseFilter(),
@ -1360,7 +1360,7 @@ class EHentai(
private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB" private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB"
private val MATCH_YEAR_REGEX = "^\\d{4}\$".toRegex() private val MATCH_YEAR_REGEX = "^\\d{4}\$".toRegex()
private val MATCH_SEEK_REGEX = "^\\d{2,4}-\\d{1,2}(-\\d{1,2})?".toRegex() private val MATCH_SEEK_REGEX = "^\\d{2,4}-\\d{1,2}".toRegex()
private val MATCH_JUMP_REGEX = "^\\d+(\$|d\$|w\$|m\$|y\$|-\$)".toRegex() private val MATCH_JUMP_REGEX = "^\\d+(\$|d\$|w\$|m\$|y\$|-\$)".toRegex()
private const val EH_API_BASE = "https://api.e-hentai.org/api.php" private const val EH_API_BASE = "https://api.e-hentai.org/api.php"

View File

@ -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( val newManga = networkToLocalManga.await(
Manga.create().copy( Manga.create().copy(
source = mangaSourceId, source = mangaSourceId,
url = mangaUrl, url = mangaUrl,

View File

@ -70,9 +70,7 @@ class NHentai(delegate: HttpSource, val context: Context) :
} }
override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) { override suspend fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val body = input.body.string() val json = GALLERY_JSON_REGEX.find(input.body.string())!!.groupValues[1].replace(
val server = MEDIA_SERVER_REGEX.find(body)?.groupValues?.get(1)?.toInt() ?: 1
val json = GALLERY_JSON_REGEX.find(body)!!.groupValues[1].replace(
UNICODE_ESCAPE_REGEX, UNICODE_ESCAPE_REGEX,
) { it.groupValues[1].toInt(radix = 16).toChar().toString() } ) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json) val jsonResponse = jsonParser.decodeFromString<JsonResponse>(json)
@ -86,8 +84,6 @@ class NHentai(delegate: HttpSource, val context: Context) :
mediaId = jsonResponse.mediaId mediaId = jsonResponse.mediaId
mediaServer = server
jsonResponse.title?.let { title -> jsonResponse.title?.let { title ->
japaneseTitle = title.japanese japaneseTitle = title.japanese
shortTitle = title.pretty shortTitle = title.pretty
@ -194,23 +190,16 @@ class NHentai(delegate: HttpSource, val context: Context) :
return PagePreviewPage( return PagePreviewPage(
page, page,
metadata.pageImageTypes.mapIndexed { index, s -> metadata.pageImageTypes.mapIndexed { index, s ->
PagePreviewInfo( PagePreviewInfo(index + 1, imageUrl = thumbnailUrlFromType(metadata.mediaId!!, index + 1, s)!!)
index + 1,
imageUrl = thumbnailUrlFromType(metadata.mediaId!!, metadata.mediaServer ?: 1, index + 1, s)!!,
)
}, },
false, false,
1, 1,
) )
} }
private fun thumbnailUrlFromType( private fun thumbnailUrlFromType(mediaId: String, page: Int, t: String) =
mediaId: String, NHentaiSearchMetadata.typeToExtension(t)?.let {
mediaServer: Int, "https://t1.nhentai.net/galleries/$mediaId/${page}t.$it"
page: Int,
t: String,
) = NHentaiSearchMetadata.typeToExtension(t)?.let {
"https://t$mediaServer.nhentai.net/galleries/$mediaId/${page}t.$it"
} }
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response { override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
@ -232,7 +221,6 @@ class NHentai(delegate: HttpSource, val context: Context) :
} }
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);") private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
private val MEDIA_SERVER_REGEX = Regex("media_server\\s*:\\s*(\\d+)")
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})") private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
private const val TITLE_PREF = "Display manga title as:" private const val TITLE_PREF = "Display manga title as:"
} }

View File

@ -7,6 +7,7 @@ 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
@ -30,7 +31,6 @@ 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,7 +251,9 @@ open class FeedScreenModel(
val result = withIOContext { val result = withIOContext {
itemUI.copy( itemUI.copy(
results = networkToLocalManga(page.map { it.toDomainManga(itemUI.source!!.id) }), results = page.map {
networkToLocalManga.await(it.toDomainManga(itemUI.source!!.id))
},
) )
} }

View File

@ -8,7 +8,6 @@ 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
@ -33,8 +32,4 @@ 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
}
} }

View File

@ -13,13 +13,13 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -45,7 +45,7 @@ fun MigrationBottomSheetDialog(
} }
class MigrationBottomSheetDialogState(private val onStartMigration: State<(extraParam: String?) -> Unit>) { class MigrationBottomSheetDialogState(private val onStartMigration: State<(extraParam: String?) -> Unit>) {
private val preferences: SourcePreferences by injectLazy() private val preferences: UnsortedPreferences by injectLazy()
/** /**
* Init general reader preferences. * Init general reader preferences.
@ -59,7 +59,6 @@ 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) }
@ -67,7 +66,6 @@ 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
@ -110,7 +108,6 @@ 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)
} }

View File

@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
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.domain.UnsortedPreferences
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
class PreMigrationScreenModel( class PreMigrationScreenModel(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val prefs: UnsortedPreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
) : ScreenModel { ) : ScreenModel {
@ -51,7 +53,7 @@ class PreMigrationScreenModel(
*/ */
private fun getEnabledSources(): List<MigrationSourceItem> { private fun getEnabledSources(): List<MigrationSourceItem> {
val languages = sourcePreferences.enabledLanguages().get() val languages = sourcePreferences.enabledLanguages().get()
val sourcesSaved = sourcePreferences.migrationSources().get().split("/") val sourcesSaved = prefs.migrationSources().get().split("/")
.mapNotNull { it.toLongOrNull() } .mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get() val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() } .mapNotNull { it.toLongOrNull() }
@ -132,6 +134,6 @@ class PreMigrationScreenModel(
?.joinToString("/") { it.source.id.toString() } ?.joinToString("/") { it.source.id.toString() }
.orEmpty() .orEmpty()
sourcePreferences.migrationSources().set(listOfSources) prefs.migrationSources().set(listOfSources)
} }
} }

View File

@ -118,7 +118,8 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
) )
val onDismissRequest = { screenModel.dialog.value = null } val onDismissRequest = { screenModel.dialog.value = null }
when ( when
(
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val dialog = dialog val dialog = dialog
) { ) {

View File

@ -8,7 +8,6 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
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.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@ -39,13 +38,14 @@ import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.history.interactor.GetHistoryByMangaId
import tachiyomi.domain.history.interactor.UpsertHistory import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
@ -64,7 +64,7 @@ import java.util.concurrent.atomic.AtomicInteger
class MigrationListScreenModel( class MigrationListScreenModel(
private val config: MigrationProcedureConfig, private val config: MigrationProcedureConfig,
private val preferences: SourcePreferences = Injekt.get(), private val preferences: UnsortedPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
@ -75,7 +75,7 @@ class MigrationListScreenModel(
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getHistoryByMangaId: GetHistory = Injekt.get(), private val getHistoryByMangaId: GetHistoryByMangaId = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(), private val upsertHistory: UpsertHistory = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
@ -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(searchResult) val localManga = networkToLocalManga.await(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)
@ -236,7 +236,7 @@ class MigrationListScreenModel(
try { try {
syncChaptersWithSource.await(chapters, localManga, source) syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) { } catch (e: Exception) {
return@async2 null return@async2 null
} }
manga.progress.value = manga.progress.value =
@ -248,7 +248,7 @@ class MigrationListScreenModel(
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Ignore cancellations // Ignore cancellations
throw e throw e
} catch (_: Exception) { } catch (e: Exception) {
null null
} }
} }
@ -264,7 +264,7 @@ class MigrationListScreenModel(
} }
if (searchResult != null) { if (searchResult != null) {
val localManga = networkToLocalManga(searchResult) val localManga = networkToLocalManga.await(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)
@ -283,7 +283,7 @@ class MigrationListScreenModel(
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Ignore cancellations // Ignore cancellations
throw e throw e
} catch (_: Exception) { } catch (e: Exception) {
null null
} }
manga.progress.value = validSources.size to (index + 1) manga.progress.value = validSources.size to (index + 1)
@ -293,7 +293,7 @@ class MigrationListScreenModel(
null null
} }
}.await() }.await()
} catch (_: CancellationException) { } catch (e: CancellationException) {
// Ignore canceled migrations // Ignore canceled migrations
continue continue
} }
@ -305,7 +305,7 @@ class MigrationListScreenModel(
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Ignore cancellations // Ignore cancellations
throw e throw e
} catch (_: Exception) { } catch (e: Exception) {
} }
} }
@ -455,12 +455,12 @@ 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(manga) val localManga = networkToLocalManga.await(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())
syncChaptersWithSource.await(chapters, localManga, source) syncChaptersWithSource.await(chapters, localManga, source)
} catch (_: Exception) { } catch (e: Exception) {
return@async null return@async null
} }
localManga localManga
@ -473,7 +473,7 @@ class MigrationListScreenModel(
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Ignore cancellations // Ignore cancellations
throw e throw e
} catch (_: Exception) { } catch (e: Exception) {
} }
migratingManga.searchResult.value = SearchResult.Result(result.id) migratingManga.searchResult.value = SearchResult.Result(result.id)

View File

@ -8,13 +8,13 @@ import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
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 eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.MigrateMangaScreen import eu.kanade.presentation.browse.MigrateMangaScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -44,7 +44,7 @@ data class MigrateMangaScreen(
onClickItem = { onClickItem = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
listOf(it.id), listOf(it.id),
) )

View File

@ -10,7 +10,6 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
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 eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.MigrateSourceScreen import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent import eu.kanade.presentation.components.TabContent
@ -20,6 +19,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -63,7 +63,7 @@ fun Screen.migrateSourceTab(): TabContent {
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList() manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext { withUIContext {
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
sourceMangas, sourceMangas,
) )

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.BrowseTabWrapper
import eu.kanade.presentation.util.Screen
class MigrationSourcesScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
BrowseTabWrapper(migrateSourceTab(), onBackPressed = navigator::pop)
}
}

View File

@ -28,7 +28,6 @@ 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
@ -36,7 +35,6 @@ import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
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 eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.MissingSourceScreen import eu.kanade.presentation.browse.MissingSourceScreen
import eu.kanade.presentation.browse.components.BrowseSourceToolbar import eu.kanade.presentation.browse.components.BrowseSourceToolbar
@ -65,6 +63,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import mihon.presentation.core.util.collectAsLazyPagingItems import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -151,11 +150,7 @@ data class BrowseSourceScreen(
Scaffold( Scaffold(
topBar = { topBar = {
Column( Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.pointerInput(Unit) {},
) {
BrowseSourceToolbar( BrowseSourceToolbar(
searchQuery = state.toolbarQuery, searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery, onSearchQueryChange = screenModel::setToolbarQuery,
@ -261,11 +256,14 @@ 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 duplicates = 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))
duplicates.isNotEmpty() -> screenModel.setDialog( duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates), BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
) )
else -> screenModel.addFavorite(manga) else -> screenModel.addFavorite(manga)
} }
@ -320,16 +318,15 @@ 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(it.id)) }, onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
it.id, dialog.duplicate.id,
dialog.manga.id, dialog.manga.id,
) )
// SY <-- // SY <--

View File

@ -16,6 +16,7 @@ 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
@ -29,7 +30,6 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import exh.metadata.metadata.RaisedSearchMetadata import exh.metadata.metadata.RaisedSearchMetadata
import exh.source.ExhPreferences
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.mangaDexSourceIds import exh.source.mangaDexSourceIds
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -39,6 +39,7 @@ 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
@ -49,6 +50,7 @@ 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
@ -56,6 +58,7 @@ import tachiyomi.core.common.preference.mapAsCheckboxState
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.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -64,15 +67,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.SourcePagingSource import tachiyomi.domain.source.repository.SourcePagingSourceType
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
@ -98,12 +101,13 @@ 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(),
// SY --> // SY -->
exhPreferences: ExhPreferences = Injekt.get(), unsortedPreferences: UnsortedPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(), uiPreferences: UiPreferences = Injekt.get(),
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(), private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(), private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(),
@ -117,7 +121,7 @@ open class BrowseSourceScreenModel(
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
// SY --> // SY -->
val ehentaiBrowseDisplayMode by exhPreferences.enhancedEHentaiView().asState(screenModelScope) val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState(screenModelScope)
val startExpanded by uiPreferences.expandFilters().asState(screenModelScope) val startExpanded by uiPreferences.expandFilters().asState(screenModelScope)
@ -189,9 +193,10 @@ open class BrowseSourceScreenModel(
createSourcePagingSource(listing.query ?: "", listing.filters) createSourcePagingSource(listing.query ?: "", listing.filters)
// SY <-- // SY <--
}.flow.map { pagingData -> }.flow.map { pagingData ->
pagingData.map { (manga, metadata) -> pagingData.map { (it, metadata) ->
getManga.subscribe(manga.url, manga.source) networkToLocalManga.await(it.toDomainManga(sourceId))
.map { it ?: manga } .let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
.filterNotNull()
// SY --> // SY -->
.combineMetadata(metadata) .combineMetadata(metadata)
// SY <-- // SY <--
@ -377,8 +382,8 @@ open class BrowseSourceScreenModel(
} }
// SY --> // SY -->
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSource { open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
return getRemoteManga(sourceId, query, filters) return getRemoteManga.subscribe(sourceId, query, filters)
} }
// SY <-- // SY <--
@ -394,8 +399,8 @@ open class BrowseSourceScreenModel(
.orEmpty() .orEmpty()
} }
suspend fun getDuplicateLibraryManga(manga: Manga): List<MangaWithChapterCount> { suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.invoke(manga) return getDuplicateLibraryManga.await(manga).getOrNull(0)
} }
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
@ -445,7 +450,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 duplicates: List<MangaWithChapterCount>) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : 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>>,

View File

@ -188,24 +188,22 @@ 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 = if (sortAscending != null) !sortAscending else null, sortDescending = filter.state?.ascending?.not()
onClick = { ?.takeIf { index == filter.state?.index },
) {
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()
}, }
)
} }
} }
} }

View File

@ -10,6 +10,7 @@ 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
@ -31,8 +32,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
@ -172,7 +173,9 @@ open class SourceFeedScreenModel(
} }
val titles = withIOContext { val titles = withIOContext {
networkToLocalManga(page.map { it.toDomainManga(source.id) }) page.map {
networkToLocalManga.await(it.toDomainManga(source.id))
}
} }
mutableState.update { state -> mutableState.update { state ->

View File

@ -5,6 +5,7 @@ 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
@ -24,7 +25,6 @@ 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,10 +169,9 @@ abstract class SearchScreenModel(
source.getSearchManga(1, query, source.getFilterList()) source.getSearchManga(1, query, source.getFilterList())
} }
val titles = page.mangas val titles = page.mangas.map {
.map { it.toDomainManga(source.id) } networkToLocalManga.await(it.toDomainManga(source.id))
.distinctBy { it.url } }
.let { networkToLocalManga(it) }
if (isActive) { if (isActive) {
updateItem(source, SearchItemResult.Success(titles)) updateItem(source, SearchItemResult.Success(titles))

View File

@ -4,16 +4,18 @@ 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
@ -25,6 +27,7 @@ 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) {
@ -35,7 +38,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 {
networkToLocalManga(it.toDomainManga(source.id)) getMangaFromSManga(it, source.id)
} }
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) { val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
@ -70,6 +73,11 @@ 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

View File

@ -40,7 +40,6 @@ 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
@ -175,9 +174,9 @@ class HistoryScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO val manga = getManga.await(mangaId) ?: return@launchIO
val duplicates = getDuplicateLibraryManga(manga) val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0)
if (duplicates.isNotEmpty()) { if (duplicate != null) {
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) }
return@launchIO return@launchIO
} }
@ -247,7 +246,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 duplicates: List<MangaWithChapterCount>) : Dialog data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class ChangeCategory( data class ChangeCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,

View File

@ -19,7 +19,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.history.HistoryScreen import eu.kanade.presentation.history.HistoryScreen
@ -37,6 +36,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -114,18 +114,17 @@ 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(it.id)) }, onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
it.id, dialog.duplicate.id,
dialog.manga.id, dialog.manga.id,
) )
// SY <-- // SY <--
@ -149,7 +148,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 = onDismissRequest, onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
) )
} SY <--*/ } SY <--*/
null -> {} null -> {}

View File

@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.ui.home package eu.kanade.tachiyomi.ui.home
import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.BackHandler
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
@ -16,6 +14,7 @@ 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
@ -24,20 +23,15 @@ 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.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.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
@ -59,7 +53,6 @@ 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
@ -68,10 +61,8 @@ 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() {
@ -79,11 +70,8 @@ object HomeScreen : Screen() {
private val openTabEvent = Channel<Tab>() private val openTabEvent = Channel<Tab>()
private val showBottomNavEvent = Channel<Boolean>() private val showBottomNavEvent = Channel<Boolean>()
@Suppress("ConstPropertyName") private const val TAB_FADE_DURATION = 200
private const val TabFadeDuration = 200 private const val TAB_NAVIGATOR_KEY = "HomeTabs"
@Suppress("ConstPropertyName")
private const val TabNavigatorKey = "HomeTabs"
private val TABS = listOf( private val TABS = listOf(
LibraryTab, LibraryTab,
@ -96,7 +84,6 @@ 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()
@ -107,7 +94,7 @@ object HomeScreen : Screen() {
TabNavigator( TabNavigator(
tab = LibraryTab, tab = LibraryTab,
key = TabNavigatorKey, key = TAB_NAVIGATOR_KEY,
) { tabNavigator -> ) { tabNavigator ->
// Provide usable navigator to content screen // Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) { CompositionLocalProvider(LocalNavigator provides navigator) {
@ -152,17 +139,16 @@ object HomeScreen : Screen() {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.consumeWindowInsets(contentPadding) .consumeWindowInsets(contentPadding),
.graphicsLayer {
scaleX = scale
scaleY = scale
},
) { ) {
AnimatedContent( AnimatedContent(
targetState = tabNavigator.current, targetState = tabNavigator.current,
transitionSpec = { transitionSpec = {
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith materialFadeThroughIn(
materialFadeThroughOut(durationMillis = TabFadeDuration) initialScale = 1f,
durationMillis = TAB_FADE_DURATION,
) togetherWith
materialFadeThroughOut(durationMillis = TAB_FADE_DURATION)
}, },
label = "tabContent", label = "tabContent",
) { ) {
@ -175,32 +161,10 @@ object HomeScreen : Screen() {
} }
val goToLibraryTab = { tabNavigator.current = LibraryTab } val goToLibraryTab = { tabNavigator.current = LibraryTab }
BackHandler(
var handlingBack by remember { mutableStateOf(false) } enabled = tabNavigator.current != LibraryTab,
PredictiveBackHandler( onBack = goToLibraryTab,
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 {
@ -348,6 +312,8 @@ 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,
) )
} }
} }

View File

@ -48,7 +48,6 @@ import exh.search.QueryComponent
import exh.search.SearchEngine import exh.search.SearchEngine
import exh.search.Text import exh.search.Text
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.ExhPreferences
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
import exh.source.isMetadataSource import exh.source.isMetadataSource
@ -87,6 +86,7 @@ import tachiyomi.core.common.util.lang.compareToWithCollator
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
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -144,7 +144,7 @@ class LibraryScreenModel(
private val downloadCache: DownloadCache = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
// SY --> // SY -->
private val exhPreferences: ExhPreferences = Injekt.get(), private val unsortedPreferences: UnsortedPreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
private val getMergedMangaById: GetMergedMangaById = Injekt.get(), private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
@ -260,9 +260,9 @@ class LibraryScreenModel(
// SY --> // SY -->
combine( combine(
exhPreferences.isHentaiEnabled().changes(), unsortedPreferences.isHentaiEnabled().changes(),
sourcePreferences.disabledSources().changes(), sourcePreferences.disabledSources().changes(),
exhPreferences.enableExhentai().changes(), unsortedPreferences.enableExhentai().changes(),
) { isHentaiEnabled, disabledSources, enableExhentai -> ) { isHentaiEnabled, disabledSources, enableExhentai ->
isHentaiEnabled && (EH_SOURCE_ID.toString() !in disabledSources || enableExhentai) isHentaiEnabled && (EH_SOURCE_ID.toString() !in disabledSources || enableExhentai)
} }
@ -771,7 +771,7 @@ class LibraryScreenModel(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun syncMangaToDex() { fun syncMangaToDex() {
launchIO { launchIO {
MdUtil.getEnabledMangaDex(sourcePreferences, sourceManager)?.let { mdex -> MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences, sourceManager)?.let { mdex ->
state.value.selection.fastFilter { it.manga.source in mangaDexSourceIds }.fastForEach { (manga) -> state.value.selection.fastFilter { it.manga.source in mangaDexSourceIds }.fastForEach { (manga) ->
mdex.updateFollowStatus(MdUtil.getMangaId(manga.url), FollowStatus.READING) mdex.updateFollowStatus(MdUtil.getMangaId(manga.url), FollowStatus.READING)
} }
@ -1242,7 +1242,6 @@ class LibraryScreenModel(
} }
// SY --> // SY -->
/** Returns first unread chapter of a manga */ /** Returns first unread chapter of a manga */
suspend fun getFirstUnread(manga: Manga): Chapter? { suspend fun getFirstUnread(manga: Manga): Chapter? {
return getNextChapters.await(manga.id).firstOrNull() return getNextChapters.await(manga.id).firstOrNull()
@ -1347,13 +1346,13 @@ class LibraryScreenModel(
} }
fun onAcceptSyncWarning() { fun onAcceptSyncWarning() {
exhPreferences.exhShowSyncIntro().set(false) unsortedPreferences.exhShowSyncIntro().set(false)
} }
fun openFavoritesSyncDialog() { fun openFavoritesSyncDialog() {
mutableState.update { mutableState.update {
it.copy( it.copy(
dialog = if (exhPreferences.exhShowSyncIntro().get()) { dialog = if (unsortedPreferences.exhShowSyncIntro().get()) {
Dialog.SyncFavoritesWarning Dialog.SyncFavoritesWarning
} else { } else {
Dialog.SyncFavoritesConfirm Dialog.SyncFavoritesConfirm

View File

@ -28,7 +28,6 @@ import cafe.adriel.voyager.navigator.Navigator
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
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.library.DeleteLibraryMangaDialog import eu.kanade.presentation.library.DeleteLibraryMangaDialog
import eu.kanade.presentation.library.LibrarySettingsDialog import eu.kanade.presentation.library.LibrarySettingsDialog
@ -64,6 +63,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryGroup import tachiyomi.domain.library.model.LibraryGroup
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
@ -201,7 +201,7 @@ data object LibraryTab : Tab {
screenModel.clearSelection() screenModel.clearSelection()
if (selectedMangaIds.isNotEmpty()) { if (selectedMangaIds.isNotEmpty()) {
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
selectedMangaIds, selectedMangaIds,
) )

View File

@ -89,7 +89,6 @@ import exh.log.DebugModeOverlay
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.ExhPreferences
import exh.syDebugVersion import exh.syDebugVersion
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@ -104,6 +103,7 @@ import mihon.core.migration.Migrator
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -117,7 +117,7 @@ class MainActivity : BaseActivity() {
private val preferences: BasePreferences by injectLazy() private val preferences: BasePreferences by injectLazy()
// SY --> // SY -->
private val exhPreferences: ExhPreferences by injectLazy() private val unsortedPreferences: UnsortedPreferences by injectLazy()
// SY <-- // SY <--
private val downloadCache: DownloadCache by injectLazy() private val downloadCache: DownloadCache by injectLazy()
@ -222,8 +222,8 @@ class MainActivity : BaseActivity() {
// SY --> // SY -->
initWhenIdle { initWhenIdle {
// Upload settings // Upload settings
if (exhPreferences.enableExhentai().get() && if (unsortedPreferences.enableExhentai().get() &&
exhPreferences.exhShowSettingsUploadWarning().get() unsortedPreferences.exhShowSettingsUploadWarning().get()
) { ) {
runExhConfigureDialog = true runExhConfigureDialog = true
} }
@ -335,7 +335,7 @@ class MainActivity : BaseActivity() {
} }
// SY --> // SY -->
if (!exhPreferences.isHentaiEnabled().get()) { if (!unsortedPreferences.isHentaiEnabled().get()) {
BlacklistedSources.HIDDEN_SOURCES += EH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES += EH_SOURCE_ID
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
} }

View File

@ -27,7 +27,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.NavigatorAdaptiveSheet import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.manga.ChapterSettingsDialog import eu.kanade.presentation.manga.ChapterSettingsDialog
@ -52,7 +51,6 @@ 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
@ -78,6 +76,7 @@ import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@ -206,7 +205,6 @@ 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) },
@ -261,13 +259,12 @@ 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(it.id)) }, onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
migrateManga(navigator, it, screenModel.manga!!.id) migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id)
// SY <-- // SY <--
}, },
) )
@ -470,14 +467,13 @@ class MangaScreen(
} }
// SY --> // SY -->
/** /**
* Initiates source migration for the specific manga. * Initiates source migration for the specific manga.
*/ */
private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) { private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
manga.id, manga.id,
toMangaId, toMangaId,

View File

@ -125,7 +125,6 @@ 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
@ -659,7 +658,7 @@ class MangaScreenModel(
existingManga = getManga.await(mergedManga.url, mergedManga.source) existingManga = getManga.await(mergedManga.url, mergedManga.source)
} }
mergedManga = networkToLocalManga(mergedManga) mergedManga = networkToLocalManga.await(mergedManga)
getCategories.await(originalMangaId) getCategories.await(originalMangaId)
.let { .let {
@ -784,10 +783,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 duplicates = getDuplicateLibraryManga(manga) val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0)
if (duplicates.isNotEmpty()) { if (duplicate != null) {
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) }
return@launchIO return@launchIO
} }
} }
@ -1654,7 +1653,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 duplicates: List<MangaWithChapterCount>) : Dialog data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
/* SY --> /* SY -->
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog

View File

@ -1,61 +0,0 @@
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,
)
}

View File

@ -33,9 +33,12 @@ class OnboardingScreen : Screen() {
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString) val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
BackHandler(enabled = !shownOnboardingFlow) { BackHandler(
enabled = !shownOnboardingFlow,
onBack = {
// Prevent exiting if onboarding hasn't been completed // Prevent exiting if onboarding hasn't been completed
} },
)
OnboardingScreen( OnboardingScreen(
onComplete = finishOnboarding, onComplete = finishOnboarding,

View File

@ -717,7 +717,7 @@ class ReaderActivity : BaseActivity() {
?.pages ?.pages
?.forEachIndexed { _, page -> ?.forEachIndexed { _, page ->
var shouldQueuePage = false var shouldQueuePage = false
if (page.status is Page.State.Error) { if (page.status == 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 is Page.State.Error) { if (curPage.status == Page.State.ERROR) {
toast(SYMR.strings.eh_boost_page_errored) toast(SYMR.strings.eh_boost_page_errored)
} else if (curPage.status == Page.State.LoadPage || curPage.status == Page.State.DownloadImage) { } else if (curPage.status == Page.State.LOAD_PAGE || curPage.status == Page.State.DOWNLOAD_IMAGE) {
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)

View File

@ -19,6 +19,7 @@ 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
@ -183,11 +184,6 @@ 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.
@ -697,7 +693,7 @@ class ReaderViewModel @JvmOverloads constructor(
readerChapter.requestedPage = pageIndex readerChapter.requestedPage = pageIndex
chapterPageIndex = pageIndex chapterPageIndex = pageIndex
if (!incognitoMode && page.status !is Page.State.Error) { if (!incognitoMode && page.status != Page.State.ERROR) {
readerChapter.chapter.last_page_read = pageIndex readerChapter.chapter.last_page_read = pageIndex
// SY --> // SY -->
@ -736,11 +732,11 @@ class ReaderViewModel @JvmOverloads constructor(
// SY --> // SY -->
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val chapterUpdates = unfilteredChapterList val chapterUpdates = chapterList
.filter { it.sourceOrder > readerChapter.chapter.source_order } .filter { it.chapter.source_order > readerChapter.chapter.source_order }
.map { chapter -> .map { chapter ->
ChapterUpdate( ChapterUpdate(
id = chapter.id, id = chapter.chapter.id!!,
read = true, read = true,
) )
} }
@ -756,14 +752,15 @@ 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 = unfilteredChapterList val duplicateUnreadChapters = chapterList
.mapNotNull { chapter -> .mapNotNull {
val chapter = it.chapter
if ( if (
!chapter.read && !chapter.read &&
chapter.isRecognizedNumber && chapter.isRecognizedNumber &&
chapter.chapterNumber.toFloat() == readerChapter.chapter.chapter_number chapter.chapter_number == 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 <--
@ -1071,7 +1068,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>()
@ -1115,8 +1112,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
@ -1191,7 +1188,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>()
@ -1223,8 +1220,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>()
@ -1260,7 +1257,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

View File

@ -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()

View File

@ -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()

View File

@ -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
} }
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -26,9 +26,7 @@ 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 kotlin.concurrent.atomics.AtomicInt import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.math.min import kotlin.math.min
/** /**
@ -68,7 +66,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 -->
@ -98,7 +96,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))
} }
} }
@ -114,17 +112,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 is Page.State.Error) { if (page.status == 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)
@ -132,7 +130,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)
} }
} }
@ -144,8 +142,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 is Page.State.Error) { if (page.status == 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
@ -196,7 +194,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
@ -213,21 +211,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.LoadPage page.status = Page.State.LOAD_PAGE
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.DownloadImage page.status = Page.State.DOWNLOAD_IMAGE
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(e) page.status = Page.State.ERROR
if (e is CancellationException) { if (e is CancellationException) {
throw e throw e
} }
@ -236,7 +234,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)
} }
@ -248,16 +246,15 @@ 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 = AtomicInt(0) private val idGenerator = AtomicInteger()
} }
private val identifier = idGenerator.incrementAndFetch() private val identifier = idGenerator.incrementAndGet()
override fun compareTo(other: PriorityPage): Int { override fun compareTo(other: PriorityPage): Int {
val p = other.priority.compareTo(priority) val p = other.priority.compareTo(priority)

View File

@ -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
} }
} }

View File

@ -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", false) fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true)
fun defaultReadingMode() = preferenceStore.getInt( fun defaultReadingMode() = preferenceStore.getInt(
"pref_default_reading_mode_key", "pref_default_reading_mode_key",
@ -184,6 +184,8 @@ class ReaderPreferences(
fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE) fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE)
fun archiveReaderMode() = preferenceStore.getInt("archive_reader_mode", ArchiveReaderMode.LOAD_FROM_FILE) fun archiveReaderMode() = preferenceStore.getInt("archive_reader_mode", ArchiveReaderMode.LOAD_FROM_FILE)
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
// SY <-- // SY <--
enum class FlashColor { enum class FlashColor {

View File

@ -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: ((Throwable?) -> Unit)? = null var onImageLoadError: (() -> 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(error: Throwable?) { open fun onImageLoadError() {
onImageLoadError?.invoke(error) onImageLoadError?.invoke()
} }
@CallSuper @CallSuper
@ -114,7 +114,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
onImageLoadError(e) onImageLoadError()
} }
}, },
) )
@ -290,7 +290,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
this@ReaderPageImageView.onImageLoadError(e) this@ReaderPageImageView.onImageLoadError()
} }
}, },
) )
@ -318,10 +318,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
setImage(ImageSource.bitmap(image.bitmap)) setImage(ImageSource.bitmap(image.bitmap))
isVisible = true isVisible = true
}, },
) onError = {
.listener( onImageLoadError()
onError = { _, result ->
onImageLoadError(result.throwable)
}, },
) )
.size(ViewSizeResolver(this@ReaderPageImageView)) .size(ViewSizeResolver(this@ReaderPageImageView))
@ -397,10 +395,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
isVisible = true isVisible = true
this@ReaderPageImageView.onImageLoaded() this@ReaderPageImageView.onImageLoaded()
}, },
) onError = {
.listener( this@ReaderPageImageView.onImageLoadError()
onError = { _, result ->
onImageLoadError(result.throwable)
}, },
) )
.crossfade(false) .crossfade(false)

View File

@ -5,7 +5,6 @@ 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
@ -23,14 +22,12 @@ 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
/** /**
@ -115,16 +112,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.LoadPage -> setLoading() Page.State.LOAD_PAGE -> setLoading()
Page.State.DownloadImage -> { Page.State.DOWNLOAD_IMAGE -> {
setDownloading() setDownloading()
page.progressFlow.collectLatest { value -> page.progressFlow.collectLatest { value ->
progressIndicator?.setProgress(value) progressIndicator?.setProgress(value)
} }
} }
Page.State.Ready -> setImage() Page.State.READY -> setImage()
is Page.State.Error -> setError(state.error) Page.State.ERROR -> setError()
} }
} }
} }
@ -216,7 +213,7 @@ class PagerPageHolder(
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { withUIContext {
setError(e) setError()
} }
} }
} }
@ -408,9 +405,9 @@ class PagerPageHolder(
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
private fun setError(error: Throwable?) { private fun setError() {
progressIndicator?.hide() progressIndicator?.hide()
showErrorLayout(error) showErrorLayout()
} }
override fun onImageLoaded() { override fun onImageLoaded() {
@ -421,9 +418,9 @@ class PagerPageHolder(
/** /**
* Called when an image fails to decode. * Called when an image fails to decode.
*/ */
override fun onImageLoadError(error: Throwable?) { override fun onImageLoadError() {
super.onImageLoadError(error) super.onImageLoadError()
setError(error) setError()
} }
/** /**
@ -434,7 +431,7 @@ class PagerPageHolder(
viewer.activity.hideMenu() viewer.activity.hideMenu()
} }
private fun showErrorLayout(error: Throwable?): ReaderErrorBinding { private fun showErrorLayout(): 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
@ -449,17 +446,12 @@ 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 sourceId = viewer.activity.viewModel.manga?.source val intent = WebViewActivity.newIntent(context, imageUrl)
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!!
} }

View File

@ -105,7 +105,11 @@ 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(pagerListener) pager.addOnPageChangeListener(
// SY -->
pagerListener,
// SY <--
)
pager.tapListener = { event -> pager.tapListener = { event ->
val viewPosition = IntArray(2) val viewPosition = IntArray(2)
pager.getLocationOnScreen(viewPosition) pager.getLocationOnScreen(viewPosition)
@ -287,9 +291,6 @@ 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
@ -302,10 +303,6 @@ 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)
} }
/** /**
@ -476,7 +473,13 @@ 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) {

Some files were not shown because too many files have changed in this diff Show More