Compare commits

..

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

312 changed files with 2373 additions and 6750 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

@ -1,8 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website - name: 🖥️ Mihon website
url: https://mihon.app/ url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions about: Guides, troubleshooting, and answers to common questions

View File

@ -53,7 +53,7 @@ body:
label: TachiyomiSY version label: TachiyomiSY version
description: You can find your TachiyomiSY version in **More → About**. description: You can find your TachiyomiSY version in **More → About**.
placeholder: | placeholder: |
Example: "1.12.0" Example: "1.11.0"
validations: validations:
required: true required: true
@ -96,9 +96,9 @@ body:
required: true required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I have filled out all of the requested information in this form, including specific version numbers. - label: I have filled out all of the requested information in this form, including specific version numbers.
required: true required: true
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions. - label: I understand that extensions are unaffiliated to Mihon, and will not receive any help for any source and/or extension-related issues.
required: true required: true

View File

@ -31,7 +31,7 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: I have updated the app to version **[1.12.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.11.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@ -63,8 +63,6 @@ jobs:
alias: ${{ secrets.ALIAS }} alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: '35.0.1'
- name: Clean up build artifacts - name: Clean up build artifacts
run: | run: |

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

19
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 0 * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: '2'
pr-inactive-days: '2'

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 = 72
versionName = "1.12.0" versionName = "1.11.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)
@ -310,12 +307,17 @@ dependencies {
// Koin // Koin
implementation(sylibs.koin.core) implementation(sylibs.koin.core)
implementation(sylibs.koin.android) implementation(sylibs.koin.android)
// ZXing Android Embedded
implementation(sylibs.zxing.android.embedded)
} }
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

@ -359,7 +359,7 @@
<data android:scheme="https" /> <data android:scheme="https" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:host="pururin.me" /> <data android:host="pururin.io" />
<data android:pathPattern="/gallery/..*" /> <data android:pathPattern="/gallery/..*" />
</intent-filter> </intent-filter>
@ -413,10 +413,6 @@
android:scheme="tachiyomisy" /> android:scheme="tachiyomisy" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
tools:remove="screenOrientation" />
</application> </application>
<uses-sdk tools:overrideLibrary="rikka.shizuku.api" <uses-sdk tools:overrideLibrary="rikka.shizuku.api"

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

@ -173,8 +173,7 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch } .associate { it.chapterNumber to it.dateFetch }
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() val markDuplicateAsRead = libraryPreferences.markDuplicateChapterRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common. // Sources MUST return the chapters from most to less recent, which is common.

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

@ -310,9 +310,9 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState() val columns by columnPreference.collectAsState()
SliderItem( SliderItem(
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns), label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns,
valueText = if (columns > 0) { valueText = if (columns > 0) {
columns.toString() columns.toString()
} else { } else {
@ -328,10 +328,6 @@ private fun ColumnScope.DisplayPage(
label = stringResource(MR.strings.action_display_download_badge), label = stringResource(MR.strings.action_display_download_badge),
pref = screenModel.libraryPreferences.downloadBadge(), pref = screenModel.libraryPreferences.downloadBadge(),
) )
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = screenModel.libraryPreferences.unreadBadge(),
)
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge), label = stringResource(MR.strings.action_display_local_badge),
pref = screenModel.libraryPreferences.localBadge(), pref = screenModel.libraryPreferences.localBadge(),

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,310 +46,81 @@ 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)),
contentPadding = horizontalPadding,
) {
items(
items = duplicates,
key = { it.manga.id },
) {
DuplicateMangaListItem(
duplicate = it,
getSource = { sourceManager.getOrStub(it.manga.source) },
onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it.manga) },
)
}
}
Column(modifier = horizontalPaddingModifier) { TextPreferenceWidget(
HorizontalDivider() title = stringResource(MR.strings.action_show_manga),
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
TextPreferenceWidget( HorizontalDivider()
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
}
OutlinedButton( TextPreferenceWidget(
onClick = onDismissRequest, title = stringResource(MR.strings.action_migrate_duplicate),
modifier = Modifier icon = Icons.Outlined.SwapVert,
.then(horizontalPaddingModifier) onPreferenceClick = {
.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() onDismissRequest()
onMigrate() onMigrate()
}, },
) )
.padding(MaterialTheme.padding.small),
) { HorizontalDivider()
Box {
MangaCover.Book( TextPreferenceWidget(
data = ImageRequest.Builder(LocalContext.current) title = stringResource(MR.strings.action_add_anyway),
.data(manga) icon = Icons.Outlined.Add,
.crossfade(true) onPreferenceClick = {
.build(), onDismissRequest()
modifier = Modifier.fillMaxWidth(), onConfirm()
},
) )
BadgeGroup(
Row(
modifier = Modifier modifier = Modifier
.padding(4.dp) .sizeIn(minHeight = minHeight)
.align(Alignment.TopStart), .clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) { ) {
Badge( OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
color = MaterialTheme.colorScheme.secondary, Text(
textColor = MaterialTheme.colorScheme.onSecondary, modifier = Modifier
text = pluralStringResource( .padding(vertical = 8.dp),
MR.plurals.manga_num_chapters, text = stringResource(MR.strings.action_cancel),
duplicate.chapterCount.toInt(), color = MaterialTheme.colorScheme.primary,
duplicate.chapterCount, style = MaterialTheme.typography.titleLarge,
), fontSize = 16.sp,
) )
}
} }
} }
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(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
if (source is StubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = source.name,
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
} }
} }
@Composable private val PaddingSize = 16.dp
private fun MangaDetailRow(
text: String,
iconImageVector: ImageVector,
maxLines: Int = 1,
) {
Row(
modifier = Modifier
.secondaryItemAlpha()
.padding(top = MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = iconImageVector,
contentDescription = null,
modifier = Modifier.size(MangaDetailsIconWidth),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
}
}
@Composable private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp { private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
val density = LocalDensity.current
val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer()
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
val coverHeight = width / MangaCover.Book.ratio
val constraints = Constraints(maxWidth = width)
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
return remember(
duplicates,
density,
typography,
textMeasurer,
smallPadding,
extraSmallPadding,
coverHeight,
constraints,
detailsConstraints,
) {
duplicates.fastMaxOfOrNull {
calculateMangaCardHeight(
manga = it.manga,
density = density,
typography = typography,
textMeasurer = textMeasurer,
smallPadding = smallPadding,
extraSmallPadding = extraSmallPadding,
coverHeight = coverHeight,
constraints = constraints,
detailsConstraints = detailsConstraints,
)
}
?: 0.dp
}
}
private fun calculateMangaCardHeight(
manga: Manga,
density: Density,
typography: Typography,
textMeasurer: TextMeasurer,
smallPadding: Int,
extraSmallPadding: Int,
coverHeight: Float,
constraints: Constraints,
detailsConstraints: Constraints,
): Dp {
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
val authorHeight = if (!manga.author.isNullOrBlank()) {
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
}
private fun TextMeasurer.measureHeight(
text: String,
style: TextStyle,
maxLines: Int,
constraints: Constraints,
): Int = measure(
text = text,
style = style,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
constraints = constraints,
)
.size
.height
private val MangaCardWidth = 150.dp
private val MangaDetailsIconWidth = 16.dp

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 = {
onAllChapterSelected(false) if (isAnySelected) {
} 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 = {
onAllChapterSelected(false) if (isAnySelected) {
} 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( Bitmap.Config.HARDWARE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { } else {
Bitmap.Config.HARDWARE Bitmap.Config.ARGB_8888
} else { }
Bitmap.Config.ARGB_8888 BitmapDrawable(
}, view.context.resources,
false, 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,40 +610,25 @@ 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 { SelectionContainer {
MangaNotesSection( Text(
content = notes, text = if (expanded) expandedDescription else shrunkDescription,
expanded = expanded, maxLines = Int.MAX_VALUE,
onEditNotes = onEditNotesClicked, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
) )
SelectionContainer {
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
} }
}, },
{ {

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

@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -54,9 +53,10 @@ sealed class Preference {
*/ */
data class SliderPreference( data class SliderPreference(
val value: Int, val value: Int,
val max: Int,
val min: Int = 0,
val steps: Int = 0,
override val title: String, override val title: String,
val valueRange: IntProgression = 0..1,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val subtitle: String? = null, override val subtitle: String? = null,
override val enabled: Boolean = true, override val enabled: Boolean = true,
override val onValueChanged: suspend (value: Int) -> Boolean = { true }, override val onValueChanged: suspend (value: Int) -> Boolean = { true },

View File

@ -5,7 +5,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -14,20 +13,16 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TitleFontSize
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.BaseSliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@ -82,22 +77,19 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.SliderPreference -> { is Preference.PreferenceItem.SliderPreference -> {
BaseSliderItem( SliderItem(
label = item.title, label = item.title,
value = item.value, min = item.min,
valueRange = item.valueRange, max = item.max,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps, steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize), value = item.value,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
onChange = { onChange = {
scope.launch { scope.launch {
item.onValueChanged(it) item.onValueChanged(it)
} }
}, },
modifier = Modifier.padding( labelStyle = MaterialTheme.typography.titleLarge,
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
) )
} }
is Preference.PreferenceItem.ListPreference<*> -> { is Preference.PreferenceItem.ListPreference<*> -> {

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),
),
), ),
) )
} }
@ -402,7 +393,7 @@ object SettingsAdvancedScreen : SearchableSettings {
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = basePreferences.alwaysDecodeLongStripWithSSIV(), preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2), title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary), subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@ -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

@ -189,7 +189,8 @@ object SettingsAppearanceScreen : SearchableSettings {
} else { } else {
stringResource(MR.strings.disabled) stringResource(MR.strings.disabled)
}, },
valueRange = 0..10, min = 0,
max = 10,
onValueChanged = { onValueChanged = {
uiPreferences.previewsRowCount().set(it) uiPreferences.previewsRowCount().set(it)
true true

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

@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -43,10 +42,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri import androidx.core.net.toUri
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.google.zxing.client.android.Intents
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
@ -55,9 +51,7 @@ import eu.kanade.presentation.more.settings.screen.data.StorageInfo
import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector
import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
@ -88,6 +82,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -509,7 +504,7 @@ object SettingsDataScreen : SearchableSettings {
) )
} }
// SY --> //SY -->
@Composable @Composable
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> { private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
return listOf( return listOf(
@ -657,22 +652,6 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> { private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val qrScanLauncher = rememberLauncherForActivityResult(ScanContract()) {
if (it.contents != null && it.contents.isNotEmpty()) {
syncPreferences.clientAPIKey().set(it.contents)
}
}
val context = LocalContext.current
val scanOptions = remember {
ScanOptions().apply {
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
setOrientationLocked(false)
setPrompt(SYMR.strings.scan_qr_code.getString(context))
addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
}
}
return listOf( return listOf(
Preference.PreferenceItem.EditTextPreference( Preference.PreferenceItem.EditTextPreference(
title = stringResource(SYMR.strings.pref_sync_host), title = stringResource(SYMR.strings.pref_sync_host),
@ -688,32 +667,11 @@ object SettingsDataScreen : SearchableSettings {
true true
}, },
), ),
Preference.PreferenceItem.CustomPreference( Preference.PreferenceItem.EditTextPreference(
title = stringResource(SYMR.strings.pref_sync_api_key), title = stringResource(SYMR.strings.pref_sync_api_key),
) { subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
val values by syncPreferences.clientAPIKey().collectAsState() preference = syncPreferences.clientAPIKey(),
EditTextPreferenceWidget( ),
title = stringResource(SYMR.strings.pref_sync_api_key),
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
onConfirm = {
syncPreferences.clientAPIKey().set(it)
true
},
icon = null,
value = values,
widget = {
IconButton(
onClick = { qrScanLauncher.launch(scanOptions) },
modifier = Modifier.padding(start = TrailingWidgetBuffer),
) {
Icon(
Icons.Filled.QrCodeScanner,
contentDescription = stringResource(SYMR.strings.scan_qr_code),
)
}
},
)
},
) )
} }

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
@ -37,8 +38,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW
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
@ -58,13 +57,17 @@ 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),
getGlobalUpdateGroup(allCategories, libraryPreferences), getGlobalUpdateGroup(allCategories, libraryPreferences),
getBehaviorGroup(libraryPreferences), getChapterSwipeActionsGroup(libraryPreferences),
// SY --> // SY -->
getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences), getSortingCategory(LocalNavigator.currentOrThrow, libraryPreferences),
getMigrationCategory(unsortedPreferences),
// SY <-- // SY <--
) )
} }
@ -225,16 +228,20 @@ object SettingsLibraryScreen : SearchableSettings {
preference = libraryPreferences.newShowUpdatesCount(), preference = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge), title = stringResource(MR.strings.pref_library_update_show_tab_badge),
), ),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.markDuplicateChapterRead(),
title = stringResource(MR.strings.pref_mark_duplicate_chapter_read),
),
), ),
) )
} }
@Composable @Composable
private fun getBehaviorGroup( private fun getChapterSwipeActionsGroup(
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_behavior), title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
preference = libraryPreferences.swipeToStartAction(), preference = libraryPreferences.swipeToStartAction(),
@ -264,16 +271,6 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
title = stringResource(MR.strings.pref_chapter_swipe_end), title = stringResource(MR.strings.pref_chapter_swipe_end),
), ),
Preference.PreferenceItem.MultiSelectListPreference(
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
entries = persistentMapOf(
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
MARK_DUPLICATE_CHAPTER_READ_NEW to
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new),
),
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
),
), ),
) )
} }
@ -295,5 +292,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

@ -175,7 +175,9 @@ object SettingsReaderScreen : SearchableSettings {
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15, max = 15,
min = 1,
steps = 13,
title = stringResource(MR.strings.pref_flash_duration), title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState, enabled = flashPageState,
@ -186,7 +188,9 @@ object SettingsReaderScreen : SearchableSettings {
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = flashInterval, value = flashInterval,
valueRange = 1..10, max = 10,
min = 1,
steps = 8,
title = stringResource(MR.strings.pref_flash_page_interval), title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState, enabled = flashPageState,
@ -227,6 +231,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),
@ -378,9 +389,8 @@ object SettingsReaderScreen : SearchableSettings {
), ),
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding, value = webtoonSidePadding,
valueRange = ReaderPreferences.let { max = ReaderPreferences.WEBTOON_PADDING_MAX,
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX min = ReaderPreferences.WEBTOON_PADDING_MIN,
},
title = stringResource(MR.strings.pref_webtoon_side_padding), title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f), subtitle = numberFormat.format(webtoonSidePadding / 100f),
onValueChanged = { onValueChanged = {

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

@ -31,7 +31,6 @@ fun EditTextPreferenceWidget(
subtitle: String?, subtitle: String?,
icon: ImageVector?, icon: ImageVector?,
value: String, value: String,
widget: @Composable (() -> Unit)? = null,
onConfirm: suspend (String) -> Boolean, onConfirm: suspend (String) -> Boolean,
) { ) {
var isDialogShown by remember { mutableStateOf(false) } var isDialogShown by remember { mutableStateOf(false) }
@ -40,7 +39,6 @@ fun EditTextPreferenceWidget(
title = title, title = title,
subtitle = subtitle?.format(value), subtitle = subtitle?.format(value),
icon = icon, icon = icon,
widget = widget,
onPreferenceClick = { isDialogShown = true }, onPreferenceClick = { isDialogShown = true },
) )

View File

@ -37,10 +37,11 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (customBrightness) { if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState() val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem( SliderItem(
value = customBrightnessValue,
valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness), label = stringResource(MR.strings.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue,
valueText = customBrightnessValue.toString(),
onChange = { screenModel.preferences.customBrightnessValue().set(it) }, onChange = { screenModel.preferences.customBrightnessValue().set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
@ -54,10 +55,10 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (colorFilter) { if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState() val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem( SliderItem(
value = colorFilterValue.red,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value), label = stringResource(MR.strings.color_filter_r_value),
max = 255,
value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(),
onChange = { newRValue -> onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16) getColorValue(it, newRValue, RED_MASK, 16)
@ -66,10 +67,10 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.green,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_g_value), label = stringResource(MR.strings.color_filter_g_value),
max = 255,
value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(),
onChange = { newGValue -> onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8) getColorValue(it, newGValue, GREEN_MASK, 8)
@ -78,10 +79,10 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.blue,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_b_value), label = stringResource(MR.strings.color_filter_b_value),
max = 255,
value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(),
onChange = { newBValue -> onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0) getColorValue(it, newBValue, BLUE_MASK, 0)
@ -90,10 +91,10 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = colorFilterValue.alpha,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_a_value), label = stringResource(MR.strings.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(),
onChange = { newAValue -> onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet { screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24) getColorValue(it, newAValue, ALPHA_MASK, 24)

View File

@ -120,20 +120,24 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
if (flashPageState) { if (flashPageState) {
SliderItem( SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION, value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration), label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis), valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) }, onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
min = 1,
max = 15,
steps = 13,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SliderItem( SliderItem(
value = flashInterval, value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval), label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval), valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = { onChange = {
flashIntervalPref.set(it) flashIntervalPref.set(it)
}, },
min = 1,
max = 10,
steps = 8,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest, pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
) )
SettingsChipRow(MR.strings.pref_flash_with) { SettingsChipRow(MR.strings.pref_flash_with) {

View File

@ -193,9 +193,10 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState() val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem( SliderItem(
value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding), label = stringResource(MR.strings.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding,
valueText = numberFormat.format(webtoonSidePadding / 100f), valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = { onChange = {
screenModel.preferences.webtoonSidePadding().set(it) screenModel.preferences.webtoonSidePadding().set(it)

View File

@ -304,15 +304,6 @@ private fun SearchResultItem(
} }
}, },
) )
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
Text(
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
modifier = Modifier.secondaryItemAlpha(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
if (type.isNotBlank()) { if (type.isNotBlank()) {
SearchResultItemDetails( SearchResultItemDetails(
title = stringResource(MR.strings.track_type), title = stringResource(MR.strings.track_type),

View File

@ -5,11 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> { internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
@ -76,8 +73,6 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
} }
} }
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private fun randTrackSearch() = TrackSearch().let { private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong() it.id = Random.nextLong()
it.manga_id = Random.nextLong() it.manga_id = Random.nextLong()
@ -93,17 +88,11 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example" it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png" it.cover_url = "https://example.com/cover.png"
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS))) it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.summary = lorem((0..40).random()).joinToString() it.summary = lorem((0..40).random()).joinToString()
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
it.artists = randomNames()
it.authors = randomNames()
it it
} }
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
private fun lorem(words: Int): Sequence<String> = private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values LoremIpsum(words).values
} }

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,
transition = {
materialSharedAxisX(
forward = navigator.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
modifier = modifier, modifier = modifier,
enterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
}
},
exitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
}
},
popEnterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
}
},
popExitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
}
},
content = { screen ->
if (this.transition.targetState == this.transition.currentState) {
LaunchedEffect(Unit) {
val newScreens = navigator.items.map { it.key }
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
if (screensToDispose.isNotEmpty()) {
screensToDispose.forEach { navigator.dispose(it) }
navigator.clearEvent()
}
screenCandidatesToDispose.value = emptySet()
}
}
screen.Content()
},
) )
} }
enum class SwipeEdge {
Unknown,
Left,
Right,
}
private enum class AnimationType {
Pop,
Cancel,
}
@Composable @Composable
fun ScreenTransition( fun ScreenTransition(
navigator: Navigator, navigator: Navigator,
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
content: ScreenTransitionContent = { it.Content() }, content: ScreenTransitionContent = { it.Content() },
) { ) {
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

@ -27,9 +27,6 @@ interface Chapter : SChapter, Serializable {
var version: Long var version: Long
} }
val Chapter.isRecognizedNumber: Boolean
get() = chapter_number >= 0f
fun Chapter.toDomainChapter(): DomainChapter? { fun Chapter.toDomainChapter(): DomainChapter? {
if (id == null || manga_id == null) return null if (id == null || manga_id == null) return null
return DomainChapter( return DomainChapter(

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,13 +316,10 @@ 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() cleaned += 1 + mangaFolder.listFiles().orEmpty().size
if (mangaFolder != null) { mangaFolder.delete()
cleaned += 1 + mangaFolder.listFiles().orEmpty().size cache.removeManga(manga)
mangaFolder.delete() return cleaned
cache.removeManga(manga)
return cleaned
}
} }
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
@ -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

@ -144,19 +144,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id |id
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|title { |title {
|userPreferred |userPreferred
|} |}
@ -237,19 +224,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|month |month
|day |day
|} |}
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|} |}
|} |}
|} |}

View File

@ -19,7 +19,6 @@ data class ALManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val totalChapters: Long, val totalChapters: Long,
val averageScore: Int, val averageScore: Int,
val staff: ALStaff,
) { ) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = remoteId remote_id = remoteId
@ -39,11 +38,6 @@ data class ALManga(
"" ""
} }
} }
staff.edges.forEach {
val name = it.node.name() ?: return@forEach
if ("Story" in it.role) authors += name
if ("Art" in it.role) artists += name
}
} }
} }

View File

@ -17,8 +17,24 @@ data class ALMangaMetadataData(
@Serializable @Serializable
data class ALMangaMetadataMedia( data class ALMangaMetadataMedia(
val id: Long, val id: Long,
val title: ALStaffName, val title: ALItemTitle,
val coverImage: ItemCover, val coverImage: ItemCover,
val description: String?, val description: String?,
val staff: ALStaff, val staff: ALStaff,
) )
@Serializable
data class ALStaff(
val edges: List<ALStaffEdge>,
)
@Serializable
data class ALStaffEdge(
val role: String,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALItemTitle,
)

View File

@ -13,7 +13,6 @@ data class ALSearchItem(
val startDate: ALFuzzyDate, val startDate: ALFuzzyDate,
val chapters: Long?, val chapters: Long?,
val averageScore: Int?, val averageScore: Int?,
val staff: ALStaff,
) { ) {
fun toALManga(): ALManga = ALManga( fun toALManga(): ALManga = ALManga(
remoteId = id, remoteId = id,
@ -25,7 +24,6 @@ data class ALSearchItem(
startDateFuzzy = startDate.toEpochMilli(), startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0, totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1, averageScore = averageScore ?: -1,
staff = staff,
) )
} }
@ -38,31 +36,3 @@ data class ALItemTitle(
data class ItemCover( data class ItemCover(
val large: String, val large: String,
) )
@Serializable
data class ALStaff(
val edges: List<ALEdge>,
)
@Serializable
data class ALEdge(
val role: String,
val id: Int,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALStaffName,
)
@Serializable
data class ALStaffName(
val userPreferred: String? = null,
val native: String? = null,
val full: String? = null,
) {
operator fun invoke(): String? {
return userPreferred ?: full ?: native
}
}

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

@ -34,10 +34,6 @@ class TrackSearch : Track {
override lateinit var tracking_url: String override lateinit var tracking_url: String
var authors: List<String> = emptyList()
var artists: List<String> = emptyList()
var cover_url: String = "" var cover_url: String = ""
var summary: String = "" var summary: String = ""

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,24 +190,17 @@ 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://t3.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 {
return client.newCachelessCallWithProgress( return client.newCachelessCallWithProgress(
@ -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

@ -126,7 +126,8 @@ class Pururin(delegate: HttpSource, val context: Context) :
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"pururin.me", "pururin.io",
"www.pururin.io",
) )
override suspend fun mapUrlToMangaUrl(uri: Uri): String { override suspend fun mapUrlToMangaUrl(uri: Uri): String {

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) { ) {
!filter.state!!.ascending val ascending = if (index == filter.state?.index) {
} else { !filter.state!!.ascending
filter.state?.ascending ?: true } else {
} filter.state!!.ascending
filter.state = Filter.Sort.Selection( }
index = index, filter.state = Filter.Sort.Selection(
ascending = ascending, index = index,
) ascending = ascending,
onUpdate() )
}, onUpdate()
) }
} }
} }
} }

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