Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c755ddf3 | |||
| 3ae6c0131b | |||
| e3b43de298 | |||
| 02ff6b4e2f | |||
| ee8379b12a | |||
| 20e1cc0a7d | |||
| 78b434e794 | |||
| 52c8b260e0 | |||
| cccdc99977 | |||
| ca15d2ccc5 | |||
| b7b0ffc885 | |||
| f1a57749c4 | |||
| 26d6c09d21 | |||
| 088e3b6800 | |||
| c8ee2dbff4 | |||
| 835a21b426 | |||
| 8d67c87639 | |||
| 73cd25e8ba | |||
| e9ed861f00 | |||
| e7c1d4deef | |||
| b8eb75fc68 | |||
| 3d34f3dd2f | |||
| 01dc277877 | |||
| b809ae5c6f | |||
| 4c563122f8 | |||
| 4c1124fdb0 | |||
| bdbaecd975 | |||
| 2b8641c1dd | |||
| f1a90000b2 | |||
| 3bd0f0b447 | |||
| cb5b618266 | |||
| c71c07690a | |||
| d85d77da01 | |||
| 164d7bc70f | |||
| db51b09f80 | |||
| 5f8d03ba9b | |||
| 4cae7b27a6 | |||
| edcf939611 | |||
| 208d291b3c | |||
| 19f049189a | |||
| c855276555 | |||
| 9e244e0889 | |||
| bf7a067908 | |||
| 67c4b71b88 | |||
| 1fa8a86cce | |||
| 73eb98960f | |||
| 59704221b7 | |||
| 19c23943ec | |||
| d068559dee | |||
| 8e6b5b8bee | |||
| c99ddbe10f | |||
| 23925c4ba6 | |||
| e71f0afd99 | |||
| 014bf97248 | |||
| fc0d666366 | |||
| eb7465e6f9 | |||
| ff6ad20a77 | |||
| 17c528a206 | |||
| 63f4034a7f | |||
| 6c1bfc2177 | |||
| 45ff1f06ba | |||
| 3e287a593a | |||
| 01420154be | |||
| 1e4c596d0e | |||
| 26cb2bbbd1 | |||
| 588db79a64 | |||
| e5aaf3b31f | |||
| 9c222b128b | |||
| c66e08d43e | |||
| 493d8fc45f | |||
| 47993cb55d | |||
| 4701cbea23 | |||
| 70efd6f2bf | |||
| b85b6a713c | |||
| 6291529a10 | |||
| 160907ab52 | |||
| bccd30a80f | |||
| ef4d3e6c4d | |||
| 4fe7a1375a | |||
| 02bc195068 | |||
| 1e20913237 | |||
| 48b488fa59 | |||
| 893f3b2e34 | |||
| 25be12852f | |||
| 4f0292b000 | |||
| f669fd9205 | |||
| c3e0646b61 | |||
| 0d2cad8693 | |||
| 237916a37b | |||
| 16653f9585 | |||
| 392b1009c9 | |||
| 10c184e58a | |||
| f6b8756dc0 | |||
| f2654807a4 | |||
| ab90b75d73 | |||
| 0b58a081af | |||
| ae98a5fe58 | |||
| 74fa4e3503 | |||
| c20b0f67a8 | |||
| 098c7196de | |||
| 99a25560c1 | |||
| 79d19a2d8b | |||
| 061d0809bd | |||
| 0ce0c45cc7 | |||
| e910362b16 | |||
| 96c05bf113 | |||
| 6d2bda5c9d | |||
| 4a080fba3f | |||
| ac5b3b164f | |||
| c43d7dbb31 | |||
| d218fdfcf4 | |||
| e59af2fd1f | |||
| 3d761b5bf4 | |||
| 1892101359 | |||
| d76d25379e | |||
| c96cf4b11a | |||
| 31601f523d | |||
| f9abe20b84 | |||
| f7030ed800 | |||
| f4173b3766 | |||
| 221a564644 | |||
| 7458eff2d8 | |||
| 187245885a | |||
| e5d8c2edbc | |||
| be4aa39c8a | |||
| 014c697773 | |||
| ea733de80e | |||
| 3ac5dcd66e | |||
| 054de1cc6f | |||
| b525c0988a | |||
| 691efe0831 | |||
| 8317a30d6e | |||
| 633937b0bc | |||
| 3152d2803a | |||
| 883c90adc8 | |||
| 8e658be7d7 | |||
| c27a4e2bf5 | |||
| 257f544a89 | |||
| f8cb08ce52 | |||
| d970eea7cf | |||
| a45fad81c8 | |||
| 76d6cf129a | |||
| 879427446f | |||
| 9edd7b0c04 | |||
| d97b83fe93 | |||
| 027e6bbb05 | |||
| 4cdead8006 | |||
| aaff472317 | |||
| 9b2febcd6d | |||
| 5de355238d | |||
| 644c8ec491 | |||
| 4b516ae4c5 | |||
| 223251f868 | |||
| 9b7d6bace1 | |||
| c15b8b65e5 | |||
| 4e9eaa5e81 | |||
| 066e10246f | |||
| 3294dd6ca8 | |||
| 2315295985 | |||
| 56ddf21107 | |||
| 3f47df21b8 | |||
| 712407f524 | |||
| 3a49a0a21a | |||
| 04b3e13ba1 | |||
| 39bc2b49c0 | |||
| 9765282640 | |||
| 320a620afa | |||
| ec2a720617 | |||
| 83d2ca7a54 | |||
| 891503c793 | |||
| 43abc6c797 | |||
| f916e94a4c | |||
| fc9b2b6e1e | |||
| 66bdd70485 | |||
| 29024ea03a | |||
| 6af33905be | |||
| f97d918728 | |||
| 6a38e501d9 | |||
| 8310733c4c | |||
| 28f0946877 | |||
| e135a0dc71 | |||
| 05a65773f0 | |||
| 8d794560a0 | |||
| 4341a98413 | |||
| 24f5a36350 | |||
| f125db6973 | |||
| 4ec969657e | |||
| cb7e790086 | |||
| 9012dafed4 | |||
| 8b24d7eded | |||
| dcc537accb | |||
| ab67775f13 | |||
| e105828e07 | |||
| 3c83edaad2 | |||
| b4f278e5fa | |||
| 29ca687a26 | |||
| ed975cae63 | |||
| 4fce0944b4 | |||
| 4a52898f08 | |||
| 92b48319ed | |||
| 9e113d80f7 | |||
| f960554cf8 | |||
| 0bdea705a5 | |||
| 33361ea7f6 | |||
| 717240f53c | |||
| 5156248a96 | |||
| e074df469e | |||
| d1277ecb02 | |||
| 8bcc235490 | |||
| aaa7171c10 | |||
| 887311b440 | |||
| 1c90aac059 | |||
| 3ad9765dcf | |||
| cc934607c8 |
+6
-3
@@ -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,sq,sqm,aidl}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
@@ -23,9 +23,14 @@ ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
|||||||
ktlint_code_style = intellij_idea
|
ktlint_code_style = intellij_idea
|
||||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||||
ktlint_standard_class-signature = disabled
|
ktlint_standard_class-signature = disabled
|
||||||
|
ktlint_standard_comment-wrapping = disabled
|
||||||
ktlint_standard_discouraged-comment-location = disabled
|
ktlint_standard_discouraged-comment-location = disabled
|
||||||
ktlint_standard_function-expression-body = disabled
|
ktlint_standard_function-expression-body = disabled
|
||||||
ktlint_standard_function-signature = disabled
|
ktlint_standard_function-signature = disabled
|
||||||
|
ktlint_standard_type-argument-comment = disabled
|
||||||
|
ktlint_standard_type-parameter-comment = disabled
|
||||||
|
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||||
|
|
||||||
# SY
|
# SY
|
||||||
ktlint_standard_filename = disabled
|
ktlint_standard_filename = disabled
|
||||||
ktlint_standard_argument-list-wrapping = disabled
|
ktlint_standard_argument-list-wrapping = disabled
|
||||||
@@ -33,8 +38,6 @@ ktlint_standard_function-naming = disabled
|
|||||||
ktlint_standard_property-naming = disabled
|
ktlint_standard_property-naming = disabled
|
||||||
ktlint_standard_multiline-expression-wrapping = disabled
|
ktlint_standard_multiline-expression-wrapping = disabled
|
||||||
ktlint_standard_string-template-indent = disabled
|
ktlint_standard_string-template-indent = disabled
|
||||||
ktlint_standard_comment-wrapping = disabled
|
|
||||||
ktlint_standard_max-line-length = disabled
|
ktlint_standard_max-line-length = disabled
|
||||||
ktlint_standard_type-argument-comment = disabled
|
|
||||||
ktlint_standard_value-argument-comment = disabled
|
ktlint_standard_value-argument-comment = disabled
|
||||||
ktlint_standard_value-parameter-comment = disabled
|
ktlint_standard_value-parameter-comment = disabled
|
||||||
@@ -71,24 +71,10 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk TachiyomiSY.apk
|
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk TachiyomiSY.apk
|
||||||
sha=`sha256sum TachiyomiSY.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
|
mv app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk TachiyomiSY-arm64-v8a.apk
|
||||||
sha=`sha256sum TachiyomiSY-arm64-v8a.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
|
mv app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk TachiyomiSY-armeabi-v7a.apk
|
||||||
sha=`sha256sum TachiyomiSY-armeabi-v7a.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
|
mv app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk TachiyomiSY-x86.apk
|
||||||
sha=`sha256sum TachiyomiSY-x86.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
|
mv app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk TachiyomiSY-x86_64.apk
|
||||||
sha=`sha256sum TachiyomiSY-x86_64.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
@@ -96,19 +82,10 @@ jobs:
|
|||||||
tag_name: ${{ github.run_number }}
|
tag_name: ${{ github.run_number }}
|
||||||
name: TachiyomiSY
|
name: TachiyomiSY
|
||||||
body: |
|
body: |
|
||||||
---
|
<!-->
|
||||||
|
> [!TIP]
|
||||||
### Checksums
|
>
|
||||||
|
> ### If you are unsure which version to download then go with `TachiyomiSY.apk`
|
||||||
| Variant | SHA-256 |
|
|
||||||
| ------- | ------- |
|
|
||||||
| Universal | ${{ env.APK_UNIVERSAL_SHA }} |
|
|
||||||
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }} |
|
|
||||||
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }} |
|
|
||||||
| x86 | ${{ env.APK_X86_SHA }} |
|
|
||||||
| x86_64 | ${{ env.APK_X86_64_SHA }} |
|
|
||||||
|
|
||||||
## If you are unsure which version to choose then go with TachiyomiSY.apk
|
|
||||||
files: |
|
files: |
|
||||||
TachiyomiSY.apk
|
TachiyomiSY.apk
|
||||||
TachiyomiSY-arm64-v8a.apk
|
TachiyomiSY-arm64-v8a.apk
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ Additional features for some extensions, features include custom description, op
|
|||||||
* NHentai
|
* NHentai
|
||||||
* Puruin
|
* Puruin
|
||||||
* Tsumino
|
* Tsumino
|
||||||
|
* LANraragi
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
aidl = true
|
||||||
|
|
||||||
// Disable some unused things
|
// Disable some unused things
|
||||||
aidl = false
|
|
||||||
renderScript = false
|
renderScript = false
|
||||||
shaders = false
|
shaders = false
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,6 @@ dependencies {
|
|||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
|
||||||
implementation(libs.richeditor.compose)
|
implementation(libs.richeditor.compose)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
@@ -278,8 +277,12 @@ dependencies {
|
|||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
|
// String similarity
|
||||||
|
implementation(libs.stringSimilarity)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
@@ -288,9 +291,6 @@ dependencies {
|
|||||||
testImplementation(kotlinx.coroutines.test)
|
testImplementation(kotlinx.coroutines.test)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// Text distance (EH)
|
|
||||||
implementation(sylibs.simularity)
|
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation(platform(libs.firebase.bom))
|
implementation(platform(libs.firebase.bom))
|
||||||
implementation(libs.firebase.analytics)
|
implementation(libs.firebase.analytics)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package mihon.app.shizuku;
|
||||||
|
|
||||||
|
interface IShellInterface {
|
||||||
|
void install(in AssetFileDescriptor apk) = 1;
|
||||||
|
|
||||||
|
void destroy() = 16777114;
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
|||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
@@ -133,6 +134,11 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
addFactory { GetExcludedScanlators(get()) }
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
addFactory { SetExcludedScanlators(get()) }
|
||||||
|
addFactory {
|
||||||
|
MigrateMangaUseCase(
|
||||||
|
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
addFactory { GetApplicationRelease(get(), get()) }
|
addFactory { GetApplicationRelease(get(), get()) }
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ class SyncChaptersWithSource(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
|
dbChapter.url,
|
||||||
/* SY --> */ manga.ogTitle /* SY <-- */,
|
/* SY --> */ manga.ogTitle /* SY <-- */,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
@@ -126,12 +127,14 @@ class SyncChaptersWithSource(
|
|||||||
if (shouldRenameChapter) {
|
if (shouldRenameChapter) {
|
||||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toChangeChapter = dbChapter.copy(
|
var toChangeChapter = dbChapter.copy(
|
||||||
name = chapter.name,
|
name = chapter.name,
|
||||||
chapterNumber = chapter.chapterNumber,
|
chapterNumber = chapter.chapterNumber,
|
||||||
scanlator = chapter.scanlator,
|
scanlator = chapter.scanlator,
|
||||||
sourceOrder = chapter.sourceOrder,
|
sourceOrder = chapter.sourceOrder,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (chapter.dateUpload != 0L) {
|
if (chapter.dateUpload != 0L) {
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ fun List<Chapter>.applyFilters(
|
|||||||
val downloaded = downloadManager.isChapterDownloaded(
|
val downloaded = downloadManager.isChapterDownloaded(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
chapter.scanlator,
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
/* SY --> */ manga.ogTitle /* SY <-- */,
|
/* SY --> */ manga.ogTitle /* SY <-- */,
|
||||||
manga.source,
|
manga.source,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ fun Manga.toSManga(): SManga = SManga.create().also {
|
|||||||
it.description = ogDescription
|
it.description = ogDescription
|
||||||
it.genre = ogGenre.orEmpty().joinToString()
|
it.genre = ogGenre.orEmpty().joinToString()
|
||||||
it.status = ogStatus.toInt()
|
it.status = ogStatus.toInt()
|
||||||
//SY <--
|
// SY <--
|
||||||
it.thumbnail_url = thumbnailUrl
|
it.thumbnail_url = thumbnailUrl
|
||||||
it.initialized = initialized
|
it.initialized = initialized
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ package eu.kanade.domain.source.service
|
|||||||
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
|
||||||
class SourcePreferences(
|
class SourcePreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||||
"pref_display_mode_catalogue",
|
"pref_display_mode_catalogue",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
@@ -89,22 +91,6 @@ class SourcePreferences(
|
|||||||
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 allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
|
||||||
|
|
||||||
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
|
fun preferredMangaDexId() = preferenceStore.getString("preferred_mangaDex_id", "0")
|
||||||
@@ -116,4 +102,21 @@ class SourcePreferences(
|
|||||||
|
|
||||||
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
|
fun recommendationSearchFlags() = preferenceStore.getInt("rec_search_flags", Int.MAX_VALUE)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||||
|
|
||||||
|
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||||
|
key = "migration_flags",
|
||||||
|
defaultValue = MigrationFlag.entries.toSet(),
|
||||||
|
serializer = { MigrationFlag.toBit(it) },
|
||||||
|
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false)
|
||||||
|
|
||||||
|
fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false)
|
||||||
|
|
||||||
|
fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||||
|
|
||||||
|
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class UiPreferences(
|
|||||||
|
|
||||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||||
|
|
||||||
|
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
||||||
fun expandFilters() = preferenceStore.getBoolean("eh_expand_filters", false)
|
fun expandFilters() = preferenceStore.getBoolean("eh_expand_filters", false)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import tachiyomi.i18n.MR
|
|||||||
enum class AppTheme(val titleRes: StringResource?) {
|
enum class AppTheme(val titleRes: StringResource?) {
|
||||||
DEFAULT(MR.strings.label_default),
|
DEFAULT(MR.strings.label_default),
|
||||||
MONET(MR.strings.theme_monet),
|
MONET(MR.strings.theme_monet),
|
||||||
|
CATPPUCCIN(MR.strings.theme_catppuccin),
|
||||||
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),
|
||||||
|
|||||||
@@ -351,13 +351,17 @@ private fun ExtensionItemContent(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
|
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extension.versionName.isNotEmpty()) {
|
if (extension.versionName.isNotEmpty()) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = extension.versionName,
|
text = extension.versionName,
|
||||||
)
|
)
|
||||||
@@ -373,6 +377,8 @@ private fun ExtensionItemContent(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (warning != null) {
|
if (warning != null) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(warning).uppercase(),
|
text = stringResource(warning).uppercase(),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
@@ -380,6 +386,12 @@ private fun ExtensionItemContent(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (extension is Extension.Installed && !extension.isShared) {
|
||||||
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.ext_installer_private),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!installStep.isCompleted()) {
|
if (!installStep.isCompleted()) {
|
||||||
DotSeparatorNoSpaceText()
|
DotSeparatorNoSpaceText()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ fun GlobalSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = false,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrateMangaScreen(
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
title: String?,
|
|
||||||
state: MigrateMangaScreenModel.State,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = { scrollBehavior ->
|
|
||||||
AppBar(
|
|
||||||
title = title,
|
|
||||||
navigateUp = navigateUp,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
if (state.isEmpty) {
|
|
||||||
EmptyScreen(
|
|
||||||
stringRes = MR.strings.empty_screen,
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
MigrateMangaContent(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
state = state,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onClickCover = onClickCover,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateMangaContent(
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
state: MigrateMangaScreenModel.State,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
) {
|
|
||||||
FastScrollLazyColumn(
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
|
||||||
items(state.titles) { manga ->
|
|
||||||
MigrateMangaItem(
|
|
||||||
manga = manga,
|
|
||||||
onClickItem = onClickItem,
|
|
||||||
onClickCover = onClickCover,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MigrateMangaItem(
|
|
||||||
manga: Manga,
|
|
||||||
onClickItem: (Manga) -> Unit,
|
|
||||||
onClickCover: (Manga) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
BaseMangaListItem(
|
|
||||||
modifier = modifier,
|
|
||||||
manga = manga,
|
|
||||||
onClickItem = { onClickItem(manga) },
|
|
||||||
onClickCover = { onClickCover(manga) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,7 @@ fun MigrateSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = true,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
import androidx.compose.material.icons.outlined.ArrowForward
|
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.CopyAll
|
import androidx.compose.material.icons.outlined.CopyAll
|
||||||
import androidx.compose.material.icons.outlined.Done
|
import androidx.compose.material.icons.outlined.Done
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Help
|
import androidx.compose.material.icons.automirrored.outlined.Help
|
||||||
import androidx.compose.material.icons.filled.ViewModule
|
import androidx.compose.material.icons.filled.ViewModule
|
||||||
import androidx.compose.material.icons.outlined.Help
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fun GlobalSearchToolbar(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
hideSourceFilter: Boolean,
|
||||||
sourceFilter: SourceFilter,
|
sourceFilter: SourceFilter,
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
onlyShowHasResults: Boolean,
|
onlyShowHasResults: Boolean,
|
||||||
@@ -73,38 +74,40 @@ fun GlobalSearchToolbar(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
// TODO: make this UX better; it only applies when triggering a new search
|
// TODO: make this UX better; it only applies when triggering a new search
|
||||||
FilterChip(
|
if (!hideSourceFilter) {
|
||||||
selected = sourceFilter == SourceFilter.PinnedOnly,
|
FilterChip(
|
||||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||||
leadingIcon = {
|
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||||
Icon(
|
leadingIcon = {
|
||||||
imageVector = Icons.Outlined.PushPin,
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = Icons.Outlined.PushPin,
|
||||||
modifier = Modifier
|
contentDescription = null,
|
||||||
.size(FilterChipDefaults.IconSize),
|
modifier = Modifier
|
||||||
)
|
.size(FilterChipDefaults.IconSize),
|
||||||
},
|
)
|
||||||
label = {
|
},
|
||||||
Text(text = stringResource(MR.strings.pinned_sources))
|
label = {
|
||||||
},
|
Text(text = stringResource(MR.strings.pinned_sources))
|
||||||
)
|
},
|
||||||
FilterChip(
|
)
|
||||||
selected = sourceFilter == SourceFilter.All,
|
FilterChip(
|
||||||
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
selected = sourceFilter == SourceFilter.All,
|
||||||
leadingIcon = {
|
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||||
Icon(
|
leadingIcon = {
|
||||||
imageVector = Icons.Outlined.DoneAll,
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = Icons.Outlined.DoneAll,
|
||||||
modifier = Modifier
|
contentDescription = null,
|
||||||
.size(FilterChipDefaults.IconSize),
|
modifier = Modifier
|
||||||
)
|
.size(FilterChipDefaults.IconSize),
|
||||||
},
|
)
|
||||||
label = {
|
},
|
||||||
Text(text = stringResource(MR.strings.all))
|
label = {
|
||||||
},
|
Text(text = stringResource(MR.strings.all))
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = onlyShowHasResults,
|
selected = onlyShowHasResults,
|
||||||
|
|||||||
-1
@@ -7,7 +7,6 @@ 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.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Label
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.automirrored.outlined.Label
|
|||||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Label
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|||||||
-1
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material.icons.outlined.Label
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.SizeTransform
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -32,9 +32,10 @@ fun NavigatorAdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
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))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -15,7 +17,41 @@ fun DownloadDropdownMenu(
|
|||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
|
offset: DpOffset? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (offset != null) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
offset = offset,
|
||||||
|
content = {
|
||||||
|
DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
content = {
|
||||||
|
DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
val options = persistentListOf(
|
val options = persistentListOf(
|
||||||
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
|
||||||
@@ -25,19 +61,13 @@ fun DownloadDropdownMenu(
|
|||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
options.map { (downloadAction, string) ->
|
||||||
expanded = expanded,
|
DropdownMenuItem(
|
||||||
onDismissRequest = onDismissRequest,
|
text = { Text(text = string) },
|
||||||
modifier = modifier,
|
onClick = {
|
||||||
) {
|
onDownloadClicked(downloadAction)
|
||||||
options.map { (downloadAction, string) ->
|
onDismissRequest()
|
||||||
DropdownMenuItem(
|
},
|
||||||
text = { Text(text = string) },
|
)
|
||||||
onClick = {
|
|
||||||
onDownloadClicked(downloadAction)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ private fun ColumnScope.DisplayPage(
|
|||||||
value = columns,
|
value = columns,
|
||||||
valueRange = 0..10,
|
valueRange = 0..10,
|
||||||
label = stringResource(MR.strings.pref_library_columns),
|
label = stringResource(MR.strings.pref_library_columns),
|
||||||
valueText = if (columns > 0) {
|
valueString = if (columns > 0) {
|
||||||
columns.toString()
|
columns.toString()
|
||||||
} else {
|
} else {
|
||||||
stringResource(MR.strings.label_auto)
|
stringResource(MR.strings.label_auto)
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -15,7 +14,7 @@ internal fun LibraryComfortableGrid(
|
|||||||
items: List<LibraryItem>,
|
items: List<LibraryItem>,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -35,7 +34,7 @@ internal fun LibraryComfortableGrid(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaComfortableGridItem(
|
MangaComfortableGridItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title,
|
title = manga.title,
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -16,7 +15,7 @@ internal fun LibraryCompactGrid(
|
|||||||
showTitle: Boolean,
|
showTitle: Boolean,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -36,7 +35,7 @@ internal fun LibraryCompactGrid(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaCompactGridItem(
|
MangaCompactGridItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title.takeIf { showTitle },
|
title = manga.title.takeIf { showTitle },
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
fun LibraryContent(
|
fun LibraryContent(
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
currentPage: () -> Int,
|
currentPage: Int,
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
showPageTabs: Boolean,
|
showPageTabs: Boolean,
|
||||||
onChangeCurrentPage: (Int) -> Unit,
|
onChangeCurrentPage: (Int) -> Unit,
|
||||||
onMangaClicked: (Long) -> Unit,
|
onClickManga: (Long) -> Unit,
|
||||||
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
||||||
onToggleSelection: (LibraryManga) -> Unit,
|
onToggleSelection: (Category, LibraryManga) -> Unit,
|
||||||
onToggleRangeSelection: (LibraryManga) -> Unit,
|
onToggleRangeSelection: (Category, LibraryManga) -> Unit,
|
||||||
onRefresh: (Category?) -> Boolean,
|
onRefresh: () -> Boolean,
|
||||||
onGlobalSearchClicked: () -> Unit,
|
onGlobalSearchClicked: () -> Unit,
|
||||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
getItemCountForCategory: (Category) -> Int?,
|
||||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
@@ -53,15 +53,14 @@ fun LibraryContent(
|
|||||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
// SY -->
|
val coercedCurrentPage = remember(categories, currentPage) { currentPage.coerceIn(0, categories.lastIndex) }
|
||||||
val coercedCurrentPage = remember(categories) { currentPage().coerceIn(0, categories.lastIndex) }
|
|
||||||
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
val pagerState = rememberPagerState(coercedCurrentPage) { categories.size }
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showPageTabs && categories.size > 1) {
|
if (showPageTabs && categories.isNotEmpty() && (categories.size > 1 || !categories.first().isSystemCategory)) {
|
||||||
LaunchedEffect(categories) {
|
LaunchedEffect(categories) {
|
||||||
if (categories.size <= pagerState.currentPage) {
|
if (categories.size <= pagerState.currentPage) {
|
||||||
pagerState.scrollToPage(categories.size - 1)
|
pagerState.scrollToPage(categories.size - 1)
|
||||||
@@ -70,23 +69,20 @@ fun LibraryContent(
|
|||||||
LibraryTabs(
|
LibraryTabs(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
getItemCountForCategory = getItemCountForCategory,
|
||||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
onTabItemClick = {
|
||||||
}
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(it)
|
||||||
val notSelectionMode = selection.isEmpty()
|
}
|
||||||
val onClickManga = { manga: LibraryManga ->
|
},
|
||||||
if (notSelectionMode) {
|
)
|
||||||
onMangaClicked(manga.manga.id)
|
|
||||||
} else {
|
|
||||||
onToggleSelection(manga)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = isRefreshing,
|
refreshing = isRefreshing,
|
||||||
|
enabled = selection.isEmpty(),
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
val started = onRefresh(categories.getOrNull(currentPage()) ?: return@PullRefresh)
|
val started = onRefresh()
|
||||||
if (!started) return@PullRefresh
|
if (!started) return@PullRefresh
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Fake refresh status but hide it after a second as it's a long running task
|
// Fake refresh status but hide it after a second as it's a long running task
|
||||||
@@ -95,19 +91,25 @@ fun LibraryContent(
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = notSelectionMode,
|
|
||||||
) {
|
) {
|
||||||
LibraryPager(
|
LibraryPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
selectedManga = selection,
|
selection = selection,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||||
|
getCategoryForPage = { page -> categories[page] },
|
||||||
getDisplayMode = getDisplayMode,
|
getDisplayMode = getDisplayMode,
|
||||||
getColumnsForOrientation = getColumnsForOrientation,
|
getColumnsForOrientation = getColumnsForOrientation,
|
||||||
getLibraryForPage = getLibraryForPage,
|
getItemsForCategory = getItemsForCategory,
|
||||||
onClickManga = onClickManga,
|
onClickManga = { category, manga ->
|
||||||
|
if (selection.isNotEmpty()) {
|
||||||
|
onToggleSelection(category, manga)
|
||||||
|
} else {
|
||||||
|
onClickManga(manga.manga.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongClickManga = onToggleRangeSelection,
|
onLongClickManga = onToggleRangeSelection,
|
||||||
onClickContinueReading = onContinueReadingClicked,
|
onClickContinueReading = onContinueReadingClicked,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
@@ -18,7 +17,7 @@ import tachiyomi.presentation.core.util.plus
|
|||||||
internal fun LibraryList(
|
internal fun LibraryList(
|
||||||
items: List<LibraryItem>,
|
items: List<LibraryItem>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
selection: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
onClick: (LibraryManga) -> Unit,
|
onClick: (LibraryManga) -> Unit,
|
||||||
onLongClick: (LibraryManga) -> Unit,
|
onLongClick: (LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
@@ -45,7 +44,7 @@ internal fun LibraryList(
|
|||||||
) { libraryItem ->
|
) { libraryItem ->
|
||||||
val manga = libraryItem.libraryManga.manga
|
val manga = libraryItem.libraryManga.manga
|
||||||
MangaListItem(
|
MangaListItem(
|
||||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
isSelected = manga.id in selection,
|
||||||
title = manga.title,
|
title = manga.title,
|
||||||
coverData = MangaCover(
|
coverData = MangaCover(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.core.preference.PreferenceMutableState
|
import eu.kanade.core.preference.PreferenceMutableState
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -31,14 +32,15 @@ fun LibraryPager(
|
|||||||
state: PagerState,
|
state: PagerState,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
hasActiveFilters: Boolean,
|
hasActiveFilters: Boolean,
|
||||||
selectedManga: List<LibraryManga>,
|
selection: Set<Long>,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onGlobalSearchClicked: () -> Unit,
|
onGlobalSearchClicked: () -> Unit,
|
||||||
|
getCategoryForPage: (Int) -> Category,
|
||||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||||
onClickManga: (LibraryManga) -> Unit,
|
onClickManga: (Category, LibraryManga) -> Unit,
|
||||||
onLongClickManga: (LibraryManga) -> Unit,
|
onLongClickManga: (Category, LibraryManga) -> Unit,
|
||||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
@@ -50,9 +52,10 @@ fun LibraryPager(
|
|||||||
// To make sure only one offscreen page is being composed
|
// To make sure only one offscreen page is being composed
|
||||||
return@HorizontalPager
|
return@HorizontalPager
|
||||||
}
|
}
|
||||||
val library = getLibraryForPage(page)
|
val category = getCategoryForPage(page)
|
||||||
|
val items = getItemsForCategory(category)
|
||||||
|
|
||||||
if (library.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
LibraryPagerEmptyScreen(
|
LibraryPagerEmptyScreen(
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
hasActiveFilters = hasActiveFilters,
|
hasActiveFilters = hasActiveFilters,
|
||||||
@@ -72,12 +75,15 @@ fun LibraryPager(
|
|||||||
remember { mutableIntStateOf(0) }
|
remember { mutableIntStateOf(0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) }
|
||||||
|
val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) }
|
||||||
|
|
||||||
when (displayMode) {
|
when (displayMode) {
|
||||||
LibraryDisplayMode.List -> {
|
LibraryDisplayMode.List -> {
|
||||||
LibraryList(
|
LibraryList(
|
||||||
items = library,
|
items = items,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
@@ -87,11 +93,11 @@ fun LibraryPager(
|
|||||||
}
|
}
|
||||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||||
LibraryCompactGrid(
|
LibraryCompactGrid(
|
||||||
items = library,
|
items = items,
|
||||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
@@ -101,10 +107,10 @@ fun LibraryPager(
|
|||||||
}
|
}
|
||||||
LibraryDisplayMode.ComfortableGrid -> {
|
LibraryDisplayMode.ComfortableGrid -> {
|
||||||
LibraryComfortableGrid(
|
LibraryComfortableGrid(
|
||||||
items = library,
|
items = items,
|
||||||
columns = columns,
|
columns = columns,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
selection = selectedManga,
|
selection = selection,
|
||||||
onClick = onClickManga,
|
onClick = onClickManga,
|
||||||
onLongClick = onLongClickManga,
|
onLongClick = onLongClickManga,
|
||||||
onClickContinueReading = onClickContinueReading,
|
onClickContinueReading = onClickContinueReading,
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText
|
|||||||
internal fun LibraryTabs(
|
internal fun LibraryTabs(
|
||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
getItemCountForCategory: (Category) -> Int?,
|
||||||
onTabItemClick: (Int) -> Unit,
|
onTabItemClick: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||||
Column(
|
Column(modifier = Modifier.zIndex(2f)) {
|
||||||
modifier = Modifier.zIndex(1f),
|
|
||||||
) {
|
|
||||||
PrimaryScrollableTabRow(
|
PrimaryScrollableTabRow(
|
||||||
selectedTabIndex = currentPageIndex,
|
selectedTabIndex = currentPageIndex,
|
||||||
edgePadding = 0.dp,
|
edgePadding = 0.dp,
|
||||||
@@ -39,7 +37,7 @@ internal fun LibraryTabs(
|
|||||||
text = {
|
text = {
|
||||||
TabText(
|
TabText(
|
||||||
text = category.visualName,
|
text = category.visualName,
|
||||||
badgeCount = getNumberOfMangaForCategory(category),
|
badgeCount = getItemCountForCategory(category),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.Lanraragi
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
||||||
@@ -87,6 +88,7 @@ import exh.source.isEhBasedManga
|
|||||||
import exh.ui.metadata.adapters.EHentaiDescription
|
import exh.ui.metadata.adapters.EHentaiDescription
|
||||||
import exh.ui.metadata.adapters.EightMusesDescription
|
import exh.ui.metadata.adapters.EightMusesDescription
|
||||||
import exh.ui.metadata.adapters.HBrowseDescription
|
import exh.ui.metadata.adapters.HBrowseDescription
|
||||||
|
import exh.ui.metadata.adapters.LanraragiDescription
|
||||||
import exh.ui.metadata.adapters.MangaDexDescription
|
import exh.ui.metadata.adapters.MangaDexDescription
|
||||||
import exh.ui.metadata.adapters.NHentaiDescription
|
import exh.ui.metadata.adapters.NHentaiDescription
|
||||||
import exh.ui.metadata.adapters.PururinDescription
|
import exh.ui.metadata.adapters.PururinDescription
|
||||||
@@ -1089,6 +1091,9 @@ fun metadataDescription(source: Source): MetadataDescriptionComposable? {
|
|||||||
is Tsumino -> { state, openMetadataViewer, _ ->
|
is Tsumino -> { state, openMetadataViewer, _ ->
|
||||||
TsuminoDescription(state, openMetadataViewer)
|
TsuminoDescription(state, openMetadataViewer)
|
||||||
}
|
}
|
||||||
|
is Lanraragi -> { state, openMetadataViewer, _ ->
|
||||||
|
LanraragiDescription(state, openMetadataViewer)
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-35
@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.combinedClickable
|
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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@@ -30,7 +31,6 @@ import androidx.compose.material.icons.outlined.DoneAll
|
|||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.RemoveDone
|
import androidx.compose.material.icons.outlined.RemoveDone
|
||||||
import androidx.compose.material.icons.outlined.SwapCalls
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -52,6 +52,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
@@ -192,7 +193,7 @@ private fun RowScope.Button(
|
|||||||
targetValue = if (toConfirm) 2f else 1f,
|
targetValue = if (toConfirm) 2f else 1f,
|
||||||
label = "weight",
|
label = "weight",
|
||||||
)
|
)
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.weight(animatedWeight)
|
.weight(animatedWeight)
|
||||||
@@ -202,24 +203,28 @@ private fun RowScope.Button(
|
|||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.Center,
|
contentAlignment = Alignment.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Column(
|
||||||
imageVector = icon,
|
verticalArrangement = Arrangement.Center,
|
||||||
contentDescription = title,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
)
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = toConfirm,
|
|
||||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
|
||||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Icon(
|
||||||
text = title,
|
imageVector = icon,
|
||||||
overflow = TextOverflow.Visible,
|
contentDescription = title,
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = toConfirm,
|
||||||
|
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||||
|
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content?.invoke()
|
content?.invoke()
|
||||||
}
|
}
|
||||||
@@ -233,9 +238,9 @@ fun LibraryBottomActionMenu(
|
|||||||
onMarkAsUnreadClicked: () -> Unit,
|
onMarkAsUnreadClicked: () -> Unit,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: () -> Unit,
|
onDeleteClicked: () -> Unit,
|
||||||
|
onMigrateClicked: (() -> Unit)?,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickCleanTitles: (() -> Unit)?,
|
onClickCleanTitles: (() -> Unit)?,
|
||||||
onClickMigrate: (() -> Unit)?,
|
|
||||||
onClickCollectRecommendations: (() -> Unit)?,
|
onClickCollectRecommendations: (() -> Unit)?,
|
||||||
onClickAddToMangaDex: (() -> Unit)?,
|
onClickAddToMangaDex: (() -> Unit)?,
|
||||||
onClickResetInfo: (() -> Unit)?,
|
onClickResetInfo: (() -> Unit)?,
|
||||||
@@ -254,12 +259,11 @@ fun LibraryBottomActionMenu(
|
|||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm =
|
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||||
remember { mutableStateListOf(false, false, false, false, false /* SY --> */, false /* SY <-- */) }
|
|
||||||
var resetJob: Job? = remember { null }
|
var resetJob: Job? = remember { null }
|
||||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
@@ -270,7 +274,8 @@ fun LibraryBottomActionMenu(
|
|||||||
val showOverflow = onClickCleanTitles != null ||
|
val showOverflow = onClickCleanTitles != null ||
|
||||||
onClickAddToMangaDex != null ||
|
onClickAddToMangaDex != null ||
|
||||||
onClickResetInfo != null ||
|
onClickResetInfo != null ||
|
||||||
onClickCollectRecommendations != null
|
onClickCollectRecommendations != null ||
|
||||||
|
onMigrateClicked != null
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val moveMarkPrev = remember { !configuration.isTabletUi() }
|
val moveMarkPrev = remember { !configuration.isTabletUi() }
|
||||||
var overFlowOpen by remember { mutableStateOf(false) }
|
var overFlowOpen by remember { mutableStateOf(false) }
|
||||||
@@ -299,11 +304,11 @@ fun LibraryBottomActionMenu(
|
|||||||
onLongClick = { onLongClickItem(3) },
|
onLongClick = { onLongClickItem(3) },
|
||||||
onClick = { downloadExpanded = !downloadExpanded },
|
onClick = { downloadExpanded = !downloadExpanded },
|
||||||
) {
|
) {
|
||||||
val onDismissRequest = { downloadExpanded = false }
|
|
||||||
DownloadDropdownMenu(
|
DownloadDropdownMenu(
|
||||||
expanded = downloadExpanded,
|
expanded = downloadExpanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = { downloadExpanded = false },
|
||||||
onDownloadClicked = onDownloadClicked,
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
offset = BottomBarMenuDpOffset,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,10 +360,10 @@ fun LibraryBottomActionMenu(
|
|||||||
onClick = onClickCleanTitles,
|
onClick = onClickCleanTitles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onClickMigrate != null) {
|
if (onMigrateClicked != null) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(MR.strings.migrate)) },
|
text = { Text(stringResource(MR.strings.migrate)) },
|
||||||
onClick = onClickMigrate,
|
onClick = onMigrateClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onClickCollectRecommendations != null) {
|
if (onClickCollectRecommendations != null) {
|
||||||
@@ -388,18 +393,11 @@ fun LibraryBottomActionMenu(
|
|||||||
onLongClick = { onLongClickItem(2) },
|
onLongClick = { onLongClickItem(2) },
|
||||||
onClick = onMarkAsUnreadClicked,
|
onClick = onMarkAsUnreadClicked,
|
||||||
)
|
)
|
||||||
if (onClickMigrate != null) {
|
|
||||||
Button(
|
|
||||||
title = stringResource(MR.strings.migrate),
|
|
||||||
icon = Icons.Outlined.SwapCalls,
|
|
||||||
toConfirm = confirm[5],
|
|
||||||
onLongClick = { onLongClickItem(5) },
|
|
||||||
onClick = onClickMigrate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)
|
||||||
|
|||||||
@@ -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,18 +25,15 @@ 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
|
||||||
@@ -56,14 +50,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 +153,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 = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.CallMerge
|
import androidx.compose.material.icons.automirrored.outlined.CallMerge
|
||||||
@@ -33,7 +34,6 @@ import androidx.compose.material.icons.filled.PersonOutline
|
|||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.AttachMoney
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
import androidx.compose.material.icons.outlined.CallMerge
|
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.outlined.Done
|
import androidx.compose.material.icons.outlined.Done
|
||||||
import androidx.compose.material.icons.outlined.DoneAll
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
@@ -52,6 +52,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -67,9 +68,13 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -79,10 +84,15 @@ import coil3.request.ImageRequest
|
|||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import com.mikepenz.markdown.model.markdownAnnotator
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
import com.mikepenz.markdown.model.markdownAnnotatorConfig
|
import com.mikepenz.markdown.model.markdownAnnotatorConfig
|
||||||
|
import com.mikepenz.markdown.utils.getUnescapedTextInNode
|
||||||
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
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
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import org.intellij.markdown.MarkdownElementTypes
|
||||||
|
import org.intellij.markdown.MarkdownTokenTypes
|
||||||
|
import org.intellij.markdown.ast.findChildOfType
|
||||||
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
|
||||||
@@ -93,6 +103,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -593,8 +605,33 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val descriptionAnnotator = markdownAnnotator(
|
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||||
annotate = { content, child ->
|
annotate = { content, child ->
|
||||||
|
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||||
|
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||||
|
|
||||||
|
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||||
|
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: return@markdownAnnotator false
|
||||||
|
|
||||||
|
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||||
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||||
|
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||||
|
?.getUnescapedTextInNode(content).orEmpty()
|
||||||
|
|
||||||
|
withLink(LinkAnnotation.Url(url = url)) {
|
||||||
|
pushStyle(linkStyle)
|
||||||
|
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||||
|
append(altText)
|
||||||
|
pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return@markdownAnnotator true
|
||||||
|
}
|
||||||
|
|
||||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||||
append(content.substring(child.startOffset, child.endOffset))
|
append(content.substring(child.startOffset, child.endOffset))
|
||||||
return@markdownAnnotator true
|
return@markdownAnnotator true
|
||||||
@@ -615,10 +652,13 @@ private fun MangaSummary(
|
|||||||
onEditNotesClicked: () -> Unit,
|
onEditNotesClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||||
|
val loadImages = remember { preferences.imagesInDescription().get() }
|
||||||
val animProgress by animateFloatAsState(
|
val animProgress by animateFloatAsState(
|
||||||
targetValue = if (expanded) 1f else 0f,
|
targetValue = if (expanded) 1f else 0f,
|
||||||
label = "summary",
|
label = "summary",
|
||||||
)
|
)
|
||||||
|
var infoHeight by remember { mutableIntStateOf(0) }
|
||||||
Layout(
|
Layout(
|
||||||
modifier = modifier.clipToBounds(),
|
modifier = modifier.clipToBounds(),
|
||||||
contents = listOf(
|
contents = listOf(
|
||||||
@@ -631,21 +671,11 @@ private fun MangaSummary(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Column {
|
Column(
|
||||||
MangaNotesSection(
|
modifier = Modifier.onSizeChanged { size ->
|
||||||
content = notes,
|
infoHeight = size.height
|
||||||
expanded = true,
|
},
|
||||||
onEditNotes = onEditNotesClicked,
|
) {
|
||||||
)
|
|
||||||
MarkdownRender(
|
|
||||||
content = description,
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
annotator = descriptionAnnotator,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Column {
|
|
||||||
MangaNotesSection(
|
MangaNotesSection(
|
||||||
content = notes,
|
content = notes,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
@@ -655,7 +685,11 @@ private fun MangaSummary(
|
|||||||
MarkdownRender(
|
MarkdownRender(
|
||||||
content = description,
|
content = description,
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
annotator = descriptionAnnotator,
|
annotator = descriptionAnnotator(
|
||||||
|
loadImages = loadImages,
|
||||||
|
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
|
||||||
|
),
|
||||||
|
loadImages = loadImages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,14 +712,11 @@ private fun MangaSummary(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) { (shrunk, expanded, actual, scrim), constraints ->
|
) { (shrunk, actual, scrim), constraints ->
|
||||||
val shrunkHeight = shrunk.single()
|
val shrunkHeight = shrunk.single()
|
||||||
.measure(constraints)
|
.measure(constraints)
|
||||||
.height
|
.height
|
||||||
val expandedHeight = expanded.single()
|
val heightDelta = infoHeight - shrunkHeight
|
||||||
.measure(constraints)
|
|
||||||
.height
|
|
||||||
val heightDelta = expandedHeight - shrunkHeight
|
|
||||||
val scrimHeight = 24.dp.roundToPx()
|
val scrimHeight = 24.dp.roundToPx()
|
||||||
|
|
||||||
val actualPlaceable = actual.single()
|
val actualPlaceable = actual.single()
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Image
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
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
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.FirstBaseline
|
import androidx.compose.ui.layout.FirstBaseline
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextLinkStyles
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -32,11 +37,13 @@ import com.mikepenz.markdown.compose.elements.MarkdownTableRow
|
|||||||
import com.mikepenz.markdown.compose.elements.MarkdownText
|
import com.mikepenz.markdown.compose.elements.MarkdownText
|
||||||
import com.mikepenz.markdown.compose.elements.listDepth
|
import com.mikepenz.markdown.compose.elements.listDepth
|
||||||
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
||||||
|
import com.mikepenz.markdown.model.DefaultMarkdownInlineContent
|
||||||
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
||||||
import com.mikepenz.markdown.model.MarkdownAnnotator
|
import com.mikepenz.markdown.model.MarkdownAnnotator
|
||||||
import com.mikepenz.markdown.model.MarkdownColors
|
import com.mikepenz.markdown.model.MarkdownColors
|
||||||
import com.mikepenz.markdown.model.MarkdownPadding
|
import com.mikepenz.markdown.model.MarkdownPadding
|
||||||
import com.mikepenz.markdown.model.MarkdownTypography
|
import com.mikepenz.markdown.model.MarkdownTypography
|
||||||
|
import com.mikepenz.markdown.model.NoOpImageTransformerImpl
|
||||||
import com.mikepenz.markdown.model.markdownAnnotator
|
import com.mikepenz.markdown.model.markdownAnnotator
|
||||||
import com.mikepenz.markdown.model.rememberMarkdownState
|
import com.mikepenz.markdown.model.rememberMarkdownState
|
||||||
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
|
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
|
||||||
@@ -59,12 +66,15 @@ import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
|
|||||||
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
|
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
|
const val MARKDOWN_INLINE_IMAGE_TAG = "MARKDOWN_INLINE_IMAGE"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownRender(
|
fun MarkdownRender(
|
||||||
content: String,
|
content: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
|
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
|
||||||
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
|
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
|
||||||
|
loadImages: Boolean = true,
|
||||||
) {
|
) {
|
||||||
Markdown(
|
Markdown(
|
||||||
markdownState = rememberMarkdownState(
|
markdownState = rememberMarkdownState(
|
||||||
@@ -77,7 +87,10 @@ fun MarkdownRender(
|
|||||||
typography = getMarkdownTypography(),
|
typography = getMarkdownTypography(),
|
||||||
padding = markdownPadding,
|
padding = markdownPadding,
|
||||||
components = markdownComponents,
|
components = markdownComponents,
|
||||||
imageTransformer = Coil3ImageTransformerImpl,
|
imageTransformer = remember(loadImages) {
|
||||||
|
if (loadImages) Coil3ImageTransformerImpl else NoOpImageTransformerImpl()
|
||||||
|
},
|
||||||
|
inlineContent = getMarkdownInlineContent(),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -88,24 +101,24 @@ private fun getMarkdownColors(): MarkdownColors {
|
|||||||
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
return DefaultMarkdownColors(
|
return DefaultMarkdownColors(
|
||||||
text = MaterialTheme.colorScheme.onSurface,
|
text = MaterialTheme.colorScheme.onSurface,
|
||||||
codeText = Color.Unspecified,
|
|
||||||
inlineCodeText = Color.Unspecified,
|
|
||||||
linkText = Color.Unspecified,
|
|
||||||
codeBackground = codeBackground,
|
codeBackground = codeBackground,
|
||||||
inlineCodeBackground = codeBackground,
|
inlineCodeBackground = codeBackground,
|
||||||
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
tableText = Color.Unspecified,
|
|
||||||
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun getMarkdownLinkStyle() = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
private fun getMarkdownTypography(): MarkdownTypography {
|
private fun getMarkdownTypography(): MarkdownTypography {
|
||||||
val link = MaterialTheme.typography.bodyMedium.copy(
|
val link = getMarkdownLinkStyle()
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
return DefaultMarkdownTypography(
|
return DefaultMarkdownTypography(
|
||||||
h1 = MaterialTheme.typography.headlineMedium,
|
h1 = MaterialTheme.typography.headlineMedium,
|
||||||
h2 = MaterialTheme.typography.headlineSmall,
|
h2 = MaterialTheme.typography.headlineSmall,
|
||||||
@@ -121,7 +134,6 @@ private fun getMarkdownTypography(): MarkdownTypography {
|
|||||||
ordered = MaterialTheme.typography.bodyMedium,
|
ordered = MaterialTheme.typography.bodyMedium,
|
||||||
bullet = MaterialTheme.typography.bodyMedium,
|
bullet = MaterialTheme.typography.bodyMedium,
|
||||||
list = MaterialTheme.typography.bodyMedium,
|
list = MaterialTheme.typography.bodyMedium,
|
||||||
link = link,
|
|
||||||
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
||||||
table = MaterialTheme.typography.bodyMedium,
|
table = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@@ -216,6 +228,27 @@ private val markdownComponents = markdownComponents(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun getMarkdownInlineContent() = DefaultMarkdownInlineContent(
|
||||||
|
inlineContent = mapOf(
|
||||||
|
MARKDOWN_INLINE_IMAGE_TAG to InlineTextContent(
|
||||||
|
placeholder = Placeholder(
|
||||||
|
width = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||||
|
height = MaterialTheme.typography.bodyMedium.fontSize * 1.25,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
|
||||||
|
),
|
||||||
|
children = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Image,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
|
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
|
||||||
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
|
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ import androidx.compose.material.icons.automirrored.outlined.Label
|
|||||||
import androidx.compose.material.icons.automirrored.outlined.PlaylistAdd
|
import androidx.compose.material.icons.automirrored.outlined.PlaylistAdd
|
||||||
import androidx.compose.material.icons.outlined.CloudOff
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
import androidx.compose.material.icons.outlined.GetApp
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
|
||||||
import androidx.compose.material.icons.outlined.History
|
import androidx.compose.material.icons.outlined.History
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Label
|
|
||||||
import androidx.compose.material.icons.outlined.NewReleases
|
import androidx.compose.material.icons.outlined.NewReleases
|
||||||
import androidx.compose.material.icons.outlined.PlaylistAdd
|
|
||||||
import androidx.compose.material.icons.outlined.QueryStats
|
import androidx.compose.material.icons.outlined.QueryStats
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.Storage
|
import androidx.compose.material.icons.outlined.Storage
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ sealed class Preference {
|
|||||||
abstract val title: String
|
abstract val title: String
|
||||||
abstract val enabled: Boolean
|
abstract val enabled: Boolean
|
||||||
|
|
||||||
sealed class PreferenceItem<T> : Preference() {
|
sealed class PreferenceItem<T, R> : Preference() {
|
||||||
// SY -->
|
// SY -->
|
||||||
abstract val subtitle: CharSequence?
|
abstract val subtitle: CharSequence?
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
abstract val icon: ImageVector?
|
abstract val icon: ImageVector?
|
||||||
abstract val onValueChanged: suspend (value: T) -> Boolean
|
abstract val onValueChanged: suspend (value: T) -> R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic [PreferenceItem] that only displays texts.
|
* A basic [PreferenceItem] that only displays texts.
|
||||||
@@ -31,9 +31,9 @@ sealed class Preference {
|
|||||||
override val subtitle: CharSequence? = null,
|
override val subtitle: CharSequence? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
val onClick: (() -> Unit)? = null,
|
val onClick: (() -> Unit)? = null,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +45,7 @@ sealed class Preference {
|
|||||||
override val subtitle: CharSequence? = null,
|
override val subtitle: CharSequence? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
||||||
) : PreferenceItem<Boolean>() {
|
) : PreferenceItem<Boolean, Boolean>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +55,13 @@ sealed class Preference {
|
|||||||
data class SliderPreference(
|
data class SliderPreference(
|
||||||
val value: Int,
|
val value: Int,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
|
override val subtitle: String? = null,
|
||||||
|
val valueString: String? = null,
|
||||||
val valueRange: IntProgression = 0..1,
|
val valueRange: IntProgression = 0..1,
|
||||||
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
||||||
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) -> Unit = {},
|
||||||
) : PreferenceItem<Int>() {
|
) : PreferenceItem<Int, Unit>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ sealed class Preference {
|
|||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
||||||
) : PreferenceItem<T>() {
|
) : PreferenceItem<T, Boolean>() {
|
||||||
internal fun internalSet(value: Any) = preference.set(value as T)
|
internal fun internalSet(value: Any) = preference.set(value as T)
|
||||||
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
||||||
|
|
||||||
@@ -99,8 +100,8 @@ sealed class Preference {
|
|||||||
{ v, e -> subtitle?.format(e[v]) },
|
{ v, e -> subtitle?.format(e[v]) },
|
||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
override val onValueChanged: suspend (value: String) -> Unit = {},
|
||||||
) : PreferenceItem<String>()
|
) : PreferenceItem<String, Unit>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||||
@@ -124,7 +125,7 @@ sealed class Preference {
|
|||||||
override val icon: ImageVector? = null,
|
override val icon: ImageVector? = null,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||||
) : PreferenceItem<Set<String>>()
|
) : PreferenceItem<Set<String>, Boolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||||
@@ -135,7 +136,7 @@ sealed class Preference {
|
|||||||
override val subtitle: String? = "%s",
|
override val subtitle: String? = "%s",
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Boolean>() {
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,31 +147,31 @@ sealed class Preference {
|
|||||||
val tracker: Tracker,
|
val tracker: Tracker,
|
||||||
val login: () -> Unit,
|
val login: () -> Unit,
|
||||||
val logout: () -> Unit,
|
val logout: () -> Unit,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val title: String = ""
|
override val title: String = ""
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InfoPreference(
|
data class InfoPreference(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
) : PreferenceItem<String>() {
|
) : PreferenceItem<String, Unit>() {
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CustomPreference(
|
data class CustomPreference(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
val content: @Composable () -> Unit,
|
val content: @Composable () -> Unit,
|
||||||
) : PreferenceItem<Unit>() {
|
) : PreferenceItem<Unit, Unit>() {
|
||||||
override val enabled: Boolean = true
|
override val enabled: Boolean = true
|
||||||
override val subtitle: String? = null
|
override val subtitle: String? = null
|
||||||
override val icon: ImageVector? = null
|
override val icon: ImageVector? = null
|
||||||
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
|
override val onValueChanged: suspend (value: Unit) -> Unit = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,6 @@ sealed class Preference {
|
|||||||
override val title: String,
|
override val title: String,
|
||||||
override val enabled: Boolean = true,
|
override val enabled: Boolean = true,
|
||||||
|
|
||||||
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
val preferenceItems: ImmutableList<PreferenceItem<out Any, out Any>>,
|
||||||
) : Preference()
|
) : Preference()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatusWrapper(
|
fun StatusWrapper(
|
||||||
item: Preference.PreferenceItem<*>,
|
item: Preference.PreferenceItem<*, *>,
|
||||||
highlightKey: String?,
|
highlightKey: String?,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -56,7 +56,7 @@ fun StatusWrapper(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PreferenceItem(
|
internal fun PreferenceItem(
|
||||||
item: Preference.PreferenceItem<*>,
|
item: Preference.PreferenceItem<*, *>,
|
||||||
highlightKey: String?,
|
highlightKey: String?,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -83,17 +83,18 @@ internal fun PreferenceItem(
|
|||||||
}
|
}
|
||||||
is Preference.PreferenceItem.SliderPreference -> {
|
is Preference.PreferenceItem.SliderPreference -> {
|
||||||
BaseSliderItem(
|
BaseSliderItem(
|
||||||
label = item.title,
|
|
||||||
value = item.value,
|
value = item.value,
|
||||||
valueRange = item.valueRange,
|
valueRange = item.valueRange,
|
||||||
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
|
||||||
steps = item.steps,
|
steps = item.steps,
|
||||||
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
title = item.title,
|
||||||
|
subtitle = item.subtitle,
|
||||||
|
valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||||
onChange = {
|
onChange = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
item.onValueChanged(it)
|
item.onValueChanged(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
titleStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
horizontal = PrefsHorizontalPadding,
|
horizontal = PrefsHorizontalPadding,
|
||||||
vertical = PrefsVerticalPadding,
|
vertical = PrefsVerticalPadding,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fun PreferenceScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Preference Item
|
// Create Preference Item
|
||||||
is Preference.PreferenceItem<*> -> item {
|
is Preference.PreferenceItem<*, *> -> item {
|
||||||
PreferenceItem(
|
PreferenceItem(
|
||||||
item = preference,
|
item = preference,
|
||||||
highlightKey = highlightKey,
|
highlightKey = highlightKey,
|
||||||
|
|||||||
+17
-2
@@ -3,6 +3,7 @@ package eu.kanade.presentation.more.settings.screen
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.webkit.WebStorage
|
import android.webkit.WebStorage
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
@@ -147,9 +148,18 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.pref_manage_notifications),
|
title = stringResource(MR.strings.pref_manage_notifications),
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
// SY -->
|
||||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
val intent = Intent().apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||||
|
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||||
|
} else {
|
||||||
|
setAction("android.settings.APP_NOTIFICATION_SETTINGS")
|
||||||
|
putExtra("app_package", context.packageName)
|
||||||
|
putExtra("app_uid", context.applicationInfo.uid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -359,6 +369,11 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
||||||
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = libraryPreferences.disallowNonAsciiFilenames(),
|
||||||
|
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
|
||||||
|
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -151,6 +151,10 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||||||
formattedNow,
|
formattedNow,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
preference = uiPreferences.imagesInDescription(),
|
||||||
|
title = stringResource(MR.strings.pref_display_images_description),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,24 +129,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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -37,6 +37,8 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||||
|
|
||||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||||
|
val parallelSourceLimit by downloadPreferences.parallelSourceLimit().collectAsState()
|
||||||
|
val parallelPageLimit by downloadPreferences.parallelPageLimit().collectAsState()
|
||||||
return listOf(
|
return listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = downloadPreferences.downloadOnlyOverWifi(),
|
preference = downloadPreferences.downloadOnlyOverWifi(),
|
||||||
@@ -51,6 +53,19 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.split_tall_images),
|
title = stringResource(MR.strings.split_tall_images),
|
||||||
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = parallelSourceLimit,
|
||||||
|
valueRange = 1..10,
|
||||||
|
title = stringResource(MR.strings.pref_download_concurrent_sources),
|
||||||
|
onValueChanged = { downloadPreferences.parallelSourceLimit().set(it) },
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.SliderPreference(
|
||||||
|
value = parallelPageLimit,
|
||||||
|
valueRange = 1..15,
|
||||||
|
title = stringResource(MR.strings.pref_download_concurrent_pages),
|
||||||
|
subtitle = stringResource(MR.strings.pref_download_concurrent_pages_summary),
|
||||||
|
onValueChanged = { downloadPreferences.parallelPageLimit().set(it) },
|
||||||
|
),
|
||||||
getDeleteChaptersGroup(
|
getDeleteChaptersGroup(
|
||||||
downloadPreferences = downloadPreferences,
|
downloadPreferences = downloadPreferences,
|
||||||
categories = allCategories,
|
categories = allCategories,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode
|
import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode
|
||||||
import androidx.compose.material.icons.outlined.ChromeReaderMode
|
|
||||||
import androidx.compose.material.icons.outlined.Code
|
import androidx.compose.material.icons.outlined.Code
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.Explore
|
import androidx.compose.material.icons.outlined.Explore
|
||||||
|
|||||||
+10
-21
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -12,6 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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
|
||||||
@@ -135,11 +135,9 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(MR.strings.pref_fullscreen),
|
title = stringResource(MR.strings.pref_fullscreen),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = readerPreferences.cutoutShort(),
|
preference = readerPreferences.drawUnderCutout(),
|
||||||
title = stringResource(MR.strings.pref_cutout_short),
|
title = stringResource(MR.strings.pref_cutout_short),
|
||||||
enabled = fullscreen &&
|
enabled = LocalView.current.hasDisplayCutout() && fullscreen,
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
||||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = readerPreferences.keepScreenOn(),
|
preference = readerPreferences.keepScreenOn(),
|
||||||
@@ -177,23 +175,17 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
valueRange = 1..15,
|
valueRange = 1..15,
|
||||||
title = stringResource(MR.strings.pref_flash_duration),
|
title = stringResource(MR.strings.pref_flash_duration),
|
||||||
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
enabled = flashPageState,
|
enabled = flashPageState,
|
||||||
onValueChanged = {
|
onValueChanged = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||||
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SliderPreference(
|
Preference.PreferenceItem.SliderPreference(
|
||||||
value = flashInterval,
|
value = flashInterval,
|
||||||
valueRange = 1..10,
|
valueRange = 1..10,
|
||||||
title = stringResource(MR.strings.pref_flash_page_interval),
|
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||||
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
enabled = flashPageState,
|
enabled = flashPageState,
|
||||||
onValueChanged = {
|
onValueChanged = { flashIntervalPref.set(it) },
|
||||||
flashIntervalPref.set(it)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
preference = flashColorPref,
|
preference = flashColorPref,
|
||||||
@@ -382,11 +374,8 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
||||||
},
|
},
|
||||||
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||||
subtitle = numberFormat.format(webtoonSidePadding / 100f),
|
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||||
onValueChanged = {
|
onValueChanged = { webtoonSidePaddingPref.set(it) },
|
||||||
webtoonSidePaddingPref.set(it)
|
|
||||||
true
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
preference = readerPreferences.readerHideThreshold(),
|
preference = readerPreferences.readerHideThreshold(),
|
||||||
@@ -597,7 +586,7 @@ object SettingsReaderScreen : SearchableSettings {
|
|||||||
title = stringResource(SYMR.strings.page_layout),
|
title = stringResource(SYMR.strings.page_layout),
|
||||||
subtitle = stringResource(SYMR.strings.automatic_can_still_switch),
|
subtitle = stringResource(SYMR.strings.automatic_can_still_switch),
|
||||||
entries = ReaderPreferences.PageLayouts
|
entries = ReaderPreferences.PageLayouts
|
||||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
.mapIndexed { index, it -> index to stringResource(it) }
|
||||||
.toMap()
|
.toMap()
|
||||||
.toImmutableMap(),
|
.toImmutableMap(),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -183,7 +183,7 @@ private fun SearchResult(
|
|||||||
emptySequence()
|
emptySequence()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
|
is Preference.PreferenceItem<*, *> -> sequenceOf(null to p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Don't show info preference
|
// Don't show info preference
|
||||||
|
|||||||
+4
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -27,6 +28,7 @@ class ExtensionReposScreenModel(
|
|||||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||||
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||||
|
|
||||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||||
@@ -53,6 +55,7 @@ class ExtensionReposScreenModel(
|
|||||||
fun createRepo(baseUrl: String) {
|
fun createRepo(baseUrl: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||||
|
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
|
||||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||||
@@ -93,6 +96,7 @@ class ExtensionReposScreenModel(
|
|||||||
fun deleteRepo(baseUrl: String) {
|
fun deleteRepo(baseUrl: String) {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
deleteExtensionRepo.await(baseUrl)
|
deleteExtensionRepo.await(baseUrl)
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ fun ExtensionRepoCreateDialog(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Text(text = stringResource(MR.strings.action_add_repo_message))
|
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ class DebugInfoScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
||||||
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
|
val items = persistentListOf<Preference.PreferenceItem<out Any, out Any>>().mutate {
|
||||||
it.add(
|
it.add(
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = "Model",
|
title = "Model",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ fun ChapterListDialog(
|
|||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloaded(
|
||||||
chapterItem.chapter.name,
|
chapterItem.chapter.name,
|
||||||
chapterItem.chapter.scanlator,
|
chapterItem.chapter.scanlator,
|
||||||
|
chapterItem.chapter.url,
|
||||||
chapterItem.manga.ogTitle,
|
chapterItem.manga.ogTitle,
|
||||||
chapterItem.manga.source,
|
chapterItem.manga.source,
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-7
@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -15,13 +16,12 @@ import androidx.compose.ui.unit.sp
|
|||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PageIndicatorText(
|
fun ReaderPageIndicator(
|
||||||
// SY -->
|
currentPage: Int,
|
||||||
currentPage: String,
|
|
||||||
// SY <--
|
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (currentPage.isEmpty() || totalPages <= 0) return
|
if (currentPage <= 0 || totalPages <= 0) return
|
||||||
|
|
||||||
val text = "$currentPage / $totalPages"
|
val text = "$currentPage / $totalPages"
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ fun PageIndicatorText(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -52,10 +53,10 @@ fun PageIndicatorText(
|
|||||||
|
|
||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageIndicatorTextPreview() {
|
private fun ReaderPageIndicatorPreview() {
|
||||||
TachiyomiPreviewTheme {
|
TachiyomiPreviewTheme {
|
||||||
Surface {
|
Surface {
|
||||||
PageIndicatorText(currentPage = "10", totalPages = 69)
|
ReaderPageIndicator(currentPage = 10, totalPages = 69)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,13 @@ package eu.kanade.presentation.reader.appbars
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -13,10 +16,13 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -27,7 +33,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.reader.components.ChapterNavigator
|
import eu.kanade.presentation.reader.components.ChapterNavigator
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
@@ -36,7 +41,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
|||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
private val animationSpec = tween<IntOffset>(200)
|
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
|
||||||
|
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
enum class NavBarType {
|
enum class NavBarType {
|
||||||
@@ -65,7 +71,6 @@ fun BoxIgnoreLayoutDirection(modifier: Modifier, content: @Composable BoxScope.(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ReaderAppBars(
|
fun ReaderAppBars(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
fullscreen: Boolean,
|
|
||||||
|
|
||||||
mangaTitle: String?,
|
mangaTitle: String?,
|
||||||
chapterTitle: String?,
|
chapterTitle: String?,
|
||||||
@@ -122,11 +127,7 @@ fun ReaderAppBars(
|
|||||||
.surfaceColorAtElevation(3.dp)
|
.surfaceColorAtElevation(3.dp)
|
||||||
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
||||||
|
|
||||||
val modifierWithInsetsPadding = if (fullscreen) {
|
val modifierWithInsetsPadding = Modifier.systemBarsPadding()
|
||||||
Modifier.systemBarsPadding()
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
BoxIgnoreLayoutDirection(
|
BoxIgnoreLayoutDirection(
|
||||||
@@ -136,11 +137,11 @@ fun ReaderAppBars(
|
|||||||
visible = visible && navBarType == NavBarType.VerticalLeft,
|
visible = visible && navBarType == NavBarType.VerticalLeft,
|
||||||
enter = slideInHorizontally(
|
enter = slideInHorizontally(
|
||||||
initialOffsetX = { -it },
|
initialOffsetX = { -it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
),
|
||||||
exit = slideOutHorizontally(
|
exit = slideOutHorizontally(
|
||||||
targetOffsetX = { -it },
|
targetOffsetX = { -it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
),
|
||||||
modifier = modifierWithInsetsPadding
|
modifier = modifierWithInsetsPadding
|
||||||
.padding(bottom = 48.dp, top = 120.dp)
|
.padding(bottom = 48.dp, top = 120.dp)
|
||||||
@@ -164,11 +165,11 @@ fun ReaderAppBars(
|
|||||||
visible = visible && navBarType == NavBarType.VerticalRight,
|
visible = visible && navBarType == NavBarType.VerticalRight,
|
||||||
enter = slideInHorizontally(
|
enter = slideInHorizontally(
|
||||||
initialOffsetX = { it },
|
initialOffsetX = { it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
),
|
||||||
exit = slideOutHorizontally(
|
exit = slideOutHorizontally(
|
||||||
targetOffsetX = { it },
|
targetOffsetX = { it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
),
|
||||||
modifier = modifierWithInsetsPadding
|
modifier = modifierWithInsetsPadding
|
||||||
.padding(bottom = 48.dp, top = 120.dp)
|
.padding(bottom = 48.dp, top = 120.dp)
|
||||||
@@ -196,48 +197,23 @@ fun ReaderAppBars(
|
|||||||
visible = visible,
|
visible = visible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(
|
||||||
initialOffsetY = { -it },
|
initialOffsetY = { -it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
) + fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
exit = slideOutVertically(
|
exit = slideOutVertically(
|
||||||
targetOffsetY = { -it },
|
targetOffsetY = { -it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
) + fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
) {
|
) {
|
||||||
// SY -->
|
// SY -->
|
||||||
Column(modifierWithInsetsPadding) {
|
Column {
|
||||||
// SY <--
|
// SY <--
|
||||||
AppBar(
|
ReaderTopBar(
|
||||||
modifier = /*SY --> */ Modifier /*SY <-- */
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
.clickable(onClick = onClickTopAppBar),
|
.clickable(onClick = onClickTopAppBar),
|
||||||
backgroundColor = backgroundColor,
|
mangaTitle = mangaTitle,
|
||||||
title = mangaTitle,
|
chapterTitle = chapterTitle,
|
||||||
subtitle = chapterTitle,
|
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
/* SY --> actions = {
|
|
||||||
AppBarActions(
|
|
||||||
listOfNotNull(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(
|
|
||||||
if (bookmarked) MR.strings.action_remove_bookmark else MR.strings.action_bookmark
|
|
||||||
),
|
|
||||||
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
|
|
||||||
onClick = onToggleBookmarked,
|
|
||||||
),
|
|
||||||
onOpenInWebView?.let {
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_open_in_web_view),
|
|
||||||
onClick = it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onShare?.let {
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_share),
|
|
||||||
onClick = it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, SY <-- */
|
|
||||||
)
|
)
|
||||||
// SY -->
|
// SY -->
|
||||||
ExhUtils(
|
ExhUtils(
|
||||||
@@ -255,8 +231,8 @@ fun ReaderAppBars(
|
|||||||
onClickBoostPage = onClickBoostPage,
|
onClickBoostPage = onClickBoostPage,
|
||||||
onClickBoostPageHelp = onClickBoostPageHelp,
|
onClickBoostPageHelp = onClickBoostPageHelp,
|
||||||
)
|
)
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
@@ -265,18 +241,18 @@ fun ReaderAppBars(
|
|||||||
visible = visible,
|
visible = visible,
|
||||||
enter = slideInVertically(
|
enter = slideInVertically(
|
||||||
initialOffsetY = { it },
|
initialOffsetY = { it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
) + fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
exit = slideOutVertically(
|
exit = slideOutVertically(
|
||||||
targetOffsetY = { it },
|
targetOffsetY = { it },
|
||||||
animationSpec = animationSpec,
|
animationSpec = readerBarsSlideAnimationSpec,
|
||||||
),
|
) + fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifierWithInsetsPadding,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
if (navBarType == NavBarType.Bottom) {
|
// SY -->
|
||||||
|
if (navBarType == NavBarType.Bottom) { // <-- SY
|
||||||
ChapterNavigator(
|
ChapterNavigator(
|
||||||
isRtl = isRtl,
|
isRtl = isRtl,
|
||||||
onNextChapter = onNextChapter,
|
onNextChapter = onNextChapter,
|
||||||
@@ -291,11 +267,10 @@ fun ReaderAppBars(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
BottomReaderBar(
|
ReaderBottomBar(
|
||||||
// SY -->
|
// SY -->
|
||||||
enabledButtons = enabledButtons,
|
enabledButtons = enabledButtons,
|
||||||
// SY <--
|
// SY <--
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
readingMode = readingMode,
|
readingMode = readingMode,
|
||||||
onClickReadingMode = onClickReadingMode,
|
onClickReadingMode = onClickReadingMode,
|
||||||
orientation = orientation,
|
orientation = orientation,
|
||||||
@@ -313,6 +288,12 @@ fun ReaderAppBars(
|
|||||||
onClickShare = onShare,
|
onClickShare = onShare,
|
||||||
onClickPageLayout = onClickPageLayout,
|
onClickPageLayout = onClickPageLayout,
|
||||||
onClickShiftPage = onClickShiftPage,
|
onClickShiftPage = onClickShiftPage,
|
||||||
|
// SY <--
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small)
|
||||||
|
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-11
@@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.presentation.reader.appbars
|
package eu.kanade.presentation.reader.appbars
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FormatListNumbered
|
import androidx.compose.material.icons.outlined.FormatListNumbered
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
@@ -15,9 +12,8 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
@@ -28,11 +24,10 @@ import tachiyomi.i18n.sy.SYMR
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomReaderBar(
|
fun ReaderBottomBar(
|
||||||
// SY -->
|
// SY -->
|
||||||
enabledButtons: ImmutableSet<String>,
|
enabledButtons: ImmutableSet<String>,
|
||||||
// SY <--
|
// SY <--
|
||||||
backgroundColor: Color,
|
|
||||||
readingMode: ReadingMode,
|
readingMode: ReadingMode,
|
||||||
onClickReadingMode: () -> Unit,
|
onClickReadingMode: () -> Unit,
|
||||||
orientation: ReaderOrientation,
|
orientation: ReaderOrientation,
|
||||||
@@ -51,12 +46,11 @@ fun BottomReaderBar(
|
|||||||
onClickPageLayout: () -> Unit,
|
onClickPageLayout: () -> Unit,
|
||||||
onClickShiftPage: () -> Unit,
|
onClickShiftPage: () -> Unit,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.pointerInput(Unit) {},
|
||||||
.background(backgroundColor)
|
|
||||||
.padding(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package eu.kanade.presentation.reader.appbars
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReaderTopBar(
|
||||||
|
mangaTitle: String?,
|
||||||
|
chapterTitle: String?,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
// bookmarked: Boolean,
|
||||||
|
// onToggleBookmarked: () -> Unit,
|
||||||
|
// onOpenInWebView: (() -> Unit)?,
|
||||||
|
// onOpenInBrowser: (() -> Unit)?,
|
||||||
|
// onShare: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
title = mangaTitle,
|
||||||
|
subtitle = chapterTitle,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
/* SY ->
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
|
.apply {
|
||||||
|
add(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(
|
||||||
|
if (bookmarked) {
|
||||||
|
MR.strings.action_remove_bookmark
|
||||||
|
} else {
|
||||||
|
MR.strings.action_bookmark
|
||||||
|
},
|
||||||
|
),
|
||||||
|
icon = if (bookmarked) {
|
||||||
|
Icons.Outlined.Bookmark
|
||||||
|
} else {
|
||||||
|
Icons.Outlined.BookmarkBorder
|
||||||
|
},
|
||||||
|
onClick = onToggleBookmarked,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onOpenInWebView?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_open_in_web_view),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onOpenInBrowser?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_open_in_browser),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onShare?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.action_share),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
<- SY */
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
|||||||
pref = screenModel.preferences.customBrightness(),
|
pref = screenModel.preferences.customBrightness(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Sets the brightness of the screen. Range is [-75, 100].
|
* Sets the brightness of the screen. Range is [-75, 100].
|
||||||
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
||||||
* From 1 to 100 it sets that value as brightness.
|
* From 1 to 100 it sets that value as brightness.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.reader.settings
|
package eu.kanade.presentation.reader.settings
|
||||||
|
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -8,6 +9,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.CheckboxItem
|
import tachiyomi.presentation.core.components.CheckboxItem
|
||||||
@@ -85,10 +87,11 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
pref = screenModel.preferences.fullscreen(),
|
pref = screenModel.preferences.fullscreen(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (screenModel.hasDisplayCutout && screenModel.preferences.fullscreen().get()) {
|
val isFullscreen by screenModel.preferences.fullscreen().collectAsState()
|
||||||
|
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(MR.strings.pref_cutout_short),
|
label = stringResource(MR.strings.pref_cutout_short),
|
||||||
pref = screenModel.preferences.cutoutShort(),
|
pref = screenModel.preferences.drawUnderCutout(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||||
valueRange = 1..15,
|
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),
|
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||||
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
)
|
)
|
||||||
@@ -130,7 +133,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||||||
value = flashInterval,
|
value = flashInterval,
|
||||||
valueRange = 1..10,
|
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),
|
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||||
onChange = {
|
onChange = {
|
||||||
flashIntervalPref.set(it)
|
flashIntervalPref.set(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||||||
value = webtoonSidePadding,
|
value = webtoonSidePadding,
|
||||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
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),
|
||||||
valueText = numberFormat.format(webtoonSidePadding / 100f),
|
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||||
onChange = {
|
onChange = {
|
||||||
screenModel.preferences.webtoonSidePadding().set(it)
|
screenModel.preferences.webtoonSidePadding().set(it)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
||||||
|
import eu.kanade.presentation.theme.colorscheme.CatppuccinColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||||
@@ -77,6 +78,7 @@ private fun getThemeColorScheme(
|
|||||||
|
|
||||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||||
|
AppTheme.CATPPUCCIN to CatppuccinColorScheme,
|
||||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||||
AppTheme.LAVENDER to LavenderColorScheme,
|
AppTheme.LAVENDER to LavenderColorScheme,
|
||||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package eu.kanade.presentation.theme.colorscheme
|
||||||
|
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors for Catppuccin theme
|
||||||
|
* MIT License
|
||||||
|
* Copyright (c) 2021 Catppuccin
|
||||||
|
* https://catppuccin.com
|
||||||
|
* M3 colors generated by Material Theme Builder (https://goo.gle/material-theme-builder-web)
|
||||||
|
*
|
||||||
|
* Key colors (dark):
|
||||||
|
* Primary #CBA6F4
|
||||||
|
* Secondary #CBA6F4
|
||||||
|
* Tertiary #CBA6F4
|
||||||
|
* Neutral #181825
|
||||||
|
|
||||||
|
* Key colors (light):
|
||||||
|
* Primary #8839EF
|
||||||
|
* Secondary #8839EF
|
||||||
|
* Tertiary #8839EF
|
||||||
|
* Neutral #E6E9EF
|
||||||
|
*/
|
||||||
|
internal object CatppuccinColorScheme : BaseColorScheme() {
|
||||||
|
|
||||||
|
override val darkScheme = darkColorScheme(
|
||||||
|
primary = Color(0xFFCBA6F7),
|
||||||
|
onPrimary = Color(0xFF11111B),
|
||||||
|
primaryContainer = Color(0xFFCBA6F7),
|
||||||
|
onPrimaryContainer = Color(0xFF11111B),
|
||||||
|
secondary = Color(0xFFCBA6F7), // Unread badge
|
||||||
|
onSecondary = Color(0xFF11111B), // Unread badge text
|
||||||
|
secondaryContainer = Color(0xFF313244), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
|
onSecondaryContainer = Color(0xFFCBA6F7), // Navigation bar selector icon
|
||||||
|
tertiary = Color(0xFFCBA6F7), // Volume and brightness bars, Downloaded badge
|
||||||
|
onTertiary = Color(0xFF11111B), // Downloaded badge text
|
||||||
|
tertiaryContainer = Color(0xFF1E1E2E),
|
||||||
|
onTertiaryContainer = Color(0xFFCDD6F4),
|
||||||
|
error = Color(0xFFF38BA8),
|
||||||
|
onError = Color(0xFF11111B),
|
||||||
|
errorContainer = Color(0xFFFF0558),
|
||||||
|
onErrorContainer = Color(0xFFEF9FB4),
|
||||||
|
background = Color(0xFF181825),
|
||||||
|
onBackground = Color(0xFFCDD6F4),
|
||||||
|
surface = Color(0xFF181825),
|
||||||
|
onSurface = Color(0xFFCDD6F4),
|
||||||
|
surfaceVariant = Color(0xFF1E1E2E), // Navigation bar background (ThemePrefWidget)
|
||||||
|
onSurfaceVariant = Color(0xFFCDD6F4), // Button (unselected)
|
||||||
|
outline = Color(0xFFCBA6F7),
|
||||||
|
outlineVariant = Color(0xFF585B70), // Outlines for buttons
|
||||||
|
scrim = Color(0xFF11111B),
|
||||||
|
inverseSurface = Color(0xFFEFF1F5), // Snackbar or whatever they called
|
||||||
|
inverseOnSurface = Color(0xFF4C4F69), // Snackbar text
|
||||||
|
inversePrimary = Color(0xFF8839EF), // Snackbar accent
|
||||||
|
surfaceDim = Color(0xFF181825),
|
||||||
|
surfaceBright = Color(0xFF313244),
|
||||||
|
surfaceContainerLowest = Color(0xFF181825),
|
||||||
|
surfaceContainerLow = Color(0xFF1E1E2E), // Repo cards
|
||||||
|
surfaceContainer = Color(0xFF1E1E2E),
|
||||||
|
surfaceContainerHigh = Color(0xFF1E1E2E), // Filter menu
|
||||||
|
surfaceContainerHighest = Color(0xFF313244), // Untoggleg button bg
|
||||||
|
)
|
||||||
|
|
||||||
|
override val lightScheme = lightColorScheme(
|
||||||
|
primary = Color(0xFF8839EF),
|
||||||
|
onPrimary = Color(0xFFDCE0E8),
|
||||||
|
primaryContainer = Color(0xFF8839EF),
|
||||||
|
onPrimaryContainer = Color(0xFFDCE0E8),
|
||||||
|
secondary = Color(0xFF8839EF), // Unread badge
|
||||||
|
onSecondary = Color(0xFFDCE0E8), // Unread badge text
|
||||||
|
secondaryContainer = Color(0xFFCDD0DA), // Navigation bar selector pill & progress indicator (remaining)
|
||||||
|
onSecondaryContainer = Color(0xFF8839EF), // Navigation bar selector icon
|
||||||
|
tertiary = Color(0xFF8839EF), // Volume and brightness bars, Downloaded badge
|
||||||
|
onTertiary = Color(0xFFDCE0E8), // Downloaded badge text
|
||||||
|
tertiaryContainer = Color(0xFFEFF1F5),
|
||||||
|
onTertiaryContainer = Color(0xFF4C4F69),
|
||||||
|
error = Color(0xFFD20F39),
|
||||||
|
onError = Color(0xFFDCE0E8),
|
||||||
|
errorContainer = Color(0xFF68001C),
|
||||||
|
onErrorContainer = Color(0xFFD61C41),
|
||||||
|
background = Color(0xFFE6E9EF),
|
||||||
|
onBackground = Color(0xFF4C4F69),
|
||||||
|
surface = Color(0xFFE6E9EF),
|
||||||
|
onSurface = Color(0xFF4C4F69),
|
||||||
|
surfaceVariant = Color(0xFFEFF1F5), // Navigation bar background (ThemePrefWidget)
|
||||||
|
onSurfaceVariant = Color(0xFF4C4F69), // Button (unselected)
|
||||||
|
outline = Color(0xFF8839EF),
|
||||||
|
outlineVariant = Color(0xFFACB0BE), // Outlines for buttons
|
||||||
|
scrim = Color(0xFFDCE0E8),
|
||||||
|
inverseSurface = Color(0xFF1E1E2E), // Snackbar
|
||||||
|
inverseOnSurface = Color(0xFFCDD6F4), // Snackbar text
|
||||||
|
inversePrimary = Color(0xFFCBA6F7), // Snackbar accent
|
||||||
|
surfaceDim = Color(0xFFE6E9EF),
|
||||||
|
surfaceBright = Color(0xFFCDD0DA),
|
||||||
|
surfaceContainerLowest = Color(0xFFE6E9EF),
|
||||||
|
surfaceContainerLow = Color(0xFFEFF1F5), // Repo cards
|
||||||
|
surfaceContainer = Color(0xFFEFF1F5), // Navigation bar background
|
||||||
|
surfaceContainerHigh = Color(0xFFEFF1F5), // Filter menu
|
||||||
|
surfaceContainerHighest = Color(0xFFCDD0DA), // Untoggleg bg
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,46 +1,13 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import androidx.activity.compose.BackHandler
|
||||||
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 +16,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 +60,41 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
BackHandler(enabled = navigator.canPop, onBack = navigator::pop)
|
||||||
@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()) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ 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.os.Message
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -19,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -26,17 +29,23 @@ 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.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
||||||
|
import com.kevinnzou.web.AccompanistWebChromeClient
|
||||||
import com.kevinnzou.web.AccompanistWebViewClient
|
import com.kevinnzou.web.AccompanistWebViewClient
|
||||||
import com.kevinnzou.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
|
import com.kevinnzou.web.WebContent
|
||||||
import com.kevinnzou.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
import com.kevinnzou.web.rememberWebViewNavigator
|
import com.kevinnzou.web.WebViewNavigator
|
||||||
import com.kevinnzou.web.rememberWebViewState
|
import com.kevinnzou.web.WebViewState
|
||||||
import eu.kanade.presentation.components.AppBar
|
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.R
|
||||||
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
|
||||||
@@ -44,6 +53,18 @@ import kotlinx.coroutines.launch
|
|||||||
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
|
||||||
|
|
||||||
|
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
|
||||||
|
var state by mutableStateOf(WebViewState(webContent))
|
||||||
|
var popupMessage: Message? = null
|
||||||
|
private set
|
||||||
|
var webView: WebView? = null
|
||||||
|
|
||||||
|
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
|
||||||
|
this.popupMessage = popupMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebViewScreenContent(
|
fun WebViewScreenContent(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
@@ -55,8 +76,20 @@ fun WebViewScreenContent(
|
|||||||
headers: Map<String, String> = emptyMap(),
|
headers: Map<String, String> = emptyMap(),
|
||||||
onUrlChange: (String) -> Unit = {},
|
onUrlChange: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val navigator = rememberWebViewNavigator()
|
|
||||||
|
val windowStack = remember {
|
||||||
|
mutableStateStackOf(
|
||||||
|
WebViewWindow(
|
||||||
|
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
||||||
|
WebViewNavigator(coroutineScope),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentWindow = windowStack.lastItemOrNull!!
|
||||||
|
val navigator = currentWindow.navigator
|
||||||
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -97,31 +130,67 @@ fun WebViewScreenContent(
|
|||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?,
|
request: WebResourceRequest?,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
request?.let {
|
val url = request?.url?.toString() ?: return false
|
||||||
// Don't attempt to open blobs as webpages
|
|
||||||
if (it.url.toString().startsWith("blob:http")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore intents urls
|
// Ignore intents urls
|
||||||
if (it.url.toString().startsWith("intent://")) {
|
if (url.startsWith("intent://")) return true
|
||||||
|
|
||||||
|
// Only open valid web urls
|
||||||
|
if (url.startsWith("http") || url.startsWith("https")) {
|
||||||
|
if (url != view?.url) {
|
||||||
|
view?.loadUrl(url, headers)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with request, but with custom headers
|
|
||||||
view?.loadUrl(it.url.toString(), headers)
|
|
||||||
}
|
}
|
||||||
return super.shouldOverrideUrlLoading(view, request)
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val webChromeClient = remember {
|
||||||
|
object : AccompanistWebChromeClient() {
|
||||||
|
override fun onCreateWindow(
|
||||||
|
view: WebView,
|
||||||
|
isDialog: Boolean,
|
||||||
|
isUserGesture: Boolean,
|
||||||
|
resultMsg: Message,
|
||||||
|
): Boolean {
|
||||||
|
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
||||||
|
if (isUserGesture) {
|
||||||
|
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializePopup(webView: WebView, message: Message): WebView {
|
||||||
|
val transport = message.obj as WebView.WebViewTransport
|
||||||
|
transport.webView = webView
|
||||||
|
message.sendToTarget()
|
||||||
|
return webView
|
||||||
|
}
|
||||||
|
|
||||||
|
val popState = remember<() -> Unit> {
|
||||||
|
{
|
||||||
|
if (windowStack.size == 1) {
|
||||||
|
onNavigateUp()
|
||||||
|
} else {
|
||||||
|
windowStack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(windowStack.size > 1, popState)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Box {
|
Box {
|
||||||
Column {
|
Column {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = state.pageTitle ?: initialTitle,
|
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||||
subtitle = currentUrl,
|
subtitle = currentUrl,
|
||||||
navigateUp = onNavigateUp,
|
navigateUp = onNavigateUp,
|
||||||
navigationIcon = Icons.Outlined.Close,
|
navigationIcon = Icons.Outlined.Close,
|
||||||
@@ -164,7 +233,18 @@ fun WebViewScreenContent(
|
|||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
onClick = { onClearCookies(currentUrl) },
|
onClick = { onClearCookies(currentUrl) },
|
||||||
),
|
),
|
||||||
),
|
).builder().apply {
|
||||||
|
if (windowStack.size > 1) {
|
||||||
|
add(
|
||||||
|
0,
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(MR.strings.action_webview_close_tab),
|
||||||
|
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
|
||||||
|
onClick = popState,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -186,7 +266,7 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (val loadingState = state.loadingState) {
|
when (val loadingState = currentWindow.state.loadingState) {
|
||||||
is LoadingState.Initializing -> LinearProgressIndicator(
|
is LoadingState.Initializing -> LinearProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -203,27 +283,55 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
WebView(
|
// We need to key the WebView composable to the window object since simply updating the WebView composable will
|
||||||
state = state,
|
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
|
||||||
modifier = Modifier
|
// completely reset the WebView composable when the current window switches.
|
||||||
.fillMaxSize()
|
key(currentWindow) {
|
||||||
.padding(contentPadding),
|
WebView(
|
||||||
navigator = navigator,
|
state = currentWindow.state,
|
||||||
onCreated = { webView ->
|
modifier = Modifier
|
||||||
webView.setDefaultSettings()
|
.fillMaxSize()
|
||||||
|
.padding(contentPadding),
|
||||||
|
navigator = navigator,
|
||||||
|
onCreated = { webView ->
|
||||||
|
webView.setDefaultSettings()
|
||||||
|
|
||||||
// Debug mode (chrome://inspect/#devices)
|
// Debug mode (chrome://inspect/#devices)
|
||||||
if (BuildConfig.DEBUG &&
|
if (BuildConfig.DEBUG &&
|
||||||
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||||
) {
|
) {
|
||||||
WebView.setWebContentsDebuggingEnabled(true)
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
headers["user-agent"]?.let {
|
headers["user-agent"]?.let {
|
||||||
webView.settings.userAgentString = it
|
webView.settings.userAgentString = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
client = webClient,
|
onDispose = { webView ->
|
||||||
)
|
val window = windowStack.items.find { it.webView == webView }
|
||||||
|
if (window == null) {
|
||||||
|
// If we couldn't find any window on the stack that owns this WebView, it means that we can
|
||||||
|
// safely dispose of it because the window containing it has been closed.
|
||||||
|
webView.destroy()
|
||||||
|
} else {
|
||||||
|
// The composable is being disposed but the WebView object is not.
|
||||||
|
// When the WebView element is recomposed, we will want the WebView to resume from its state
|
||||||
|
// before it was unmounted, we won't want it to reset back to its original target.
|
||||||
|
window.state.content = WebContent.NavigatorOnly
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client = webClient,
|
||||||
|
chromeClient = webChromeClient,
|
||||||
|
factory = { context ->
|
||||||
|
currentWindow.webView
|
||||||
|
?: WebView(context).also { webView ->
|
||||||
|
currentWindow.webView = webView
|
||||||
|
currentWindow.popupMessage?.let {
|
||||||
|
initializePopup(webView, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
|
LogcatLogger.install()
|
||||||
|
LogcatLogger.loggers += XLogLogcatLogger() // SY Redirect Logcat to XLog
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
this@App,
|
this@App,
|
||||||
0,
|
0,
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
@@ -278,8 +279,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// Override the value passed as X-Requested-With in WebView requests
|
// Override the value passed as X-Requested-With in WebView requests
|
||||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||||
val isChromiumCall = stackTrace.any { trace ->
|
val isChromiumCall = stackTrace.any { trace ->
|
||||||
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
|
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
||||||
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
|
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
||||||
|
|||||||
+1
@@ -138,6 +138,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
|
|||||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||||
version = this.version,
|
version = this.version,
|
||||||
notes = this.notes,
|
notes = this.notes,
|
||||||
|
initialized = this.initialized,
|
||||||
// SY -->
|
// SY -->
|
||||||
).also { backupManga ->
|
).also { backupManga ->
|
||||||
customMangaInfo?.let {
|
customMangaInfo?.let {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ data class BackupManga(
|
|||||||
@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 = "",
|
@ProtoNumber(110) var notes: String = "",
|
||||||
|
@ProtoNumber(111) var initialized: Boolean = false,
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@@ -80,6 +81,7 @@ data class BackupManga(
|
|||||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||||
version = this@BackupManga.version,
|
version = this@BackupManga.version,
|
||||||
notes = this@BackupManga.notes,
|
notes = this@BackupManga.notes,
|
||||||
|
initialized = this@BackupManga.initialized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,9 +171,7 @@ class MangaRestorer(
|
|||||||
manga: Manga,
|
manga: Manga,
|
||||||
): Manga {
|
): Manga {
|
||||||
return manga.copy(
|
return manga.copy(
|
||||||
initialized = manga.description != null,
|
|
||||||
id = insertManga(manga),
|
id = insertManga(manga),
|
||||||
version = manga.version,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -148,7 +147,7 @@ class ChapterCache(
|
|||||||
fun isImageInCache(imageUrl: String): Boolean {
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
|
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +179,7 @@ class ChapterCache(
|
|||||||
try {
|
try {
|
||||||
// Get editor from md5 key.
|
// Get editor from md5 key.
|
||||||
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
response.body.source().saveTo(editor.newOutputStream(0))
|
response.body.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.jakewharton.disklrucache.DiskLruCache
|
|||||||
import eu.kanade.tachiyomi.source.PagePreviewPage
|
import eu.kanade.tachiyomi.source.PagePreviewPage
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.Source
|
import okio.Source
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
|||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.PagePreviewSource
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import exh.source.getMainSource
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
@@ -249,7 +250,7 @@ class PagePreviewFetcher(
|
|||||||
isInCache = { pagePreviewCache.isImageInCache(data.imageUrl) },
|
isInCache = { pagePreviewCache.isImageInCache(data.imageUrl) },
|
||||||
writeToCache = { pagePreviewCache.putImageToCache(data.imageUrl, it) },
|
writeToCache = { pagePreviewCache.putImageToCache(data.imageUrl, it) },
|
||||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.source) as? PagePreviewSource },
|
sourceLazy = lazy { sourceManager.get(data.source)?.getMainSource<PagePreviewSource>() },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
imageLoader = imageLoader,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ class DownloadCache(
|
|||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapterName the name of the chapter to query.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
* @param chapterScanlator scanlator of the chapter to query
|
||||||
|
* @param chapterUrl the url of the chapter to query
|
||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param sourceId the id of the source of the chapter.
|
* @param sourceId the id of the source of the chapter.
|
||||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
||||||
@@ -135,13 +136,14 @@ class DownloadCache(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean,
|
skipCache: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (skipCache) {
|
if (skipCache) {
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
|
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
renewCache()
|
renewCache()
|
||||||
@@ -153,6 +155,7 @@ class DownloadCache(
|
|||||||
return provider.getValidChapterDirNames(
|
return provider.getValidChapterDirNames(
|
||||||
chapterName,
|
chapterName,
|
||||||
chapterScanlator,
|
chapterScanlator,
|
||||||
|
chapterUrl,
|
||||||
).any { it in mangaDir.chapterDirs }
|
).any { it in mangaDir.chapterDirs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +242,7 @@ class DownloadCache(
|
|||||||
/* SY --> */ manga.ogTitle, /* SY <-- */
|
/* SY --> */ manga.ogTitle, /* SY <-- */
|
||||||
),
|
),
|
||||||
] ?: return
|
] ?: return
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
@@ -279,7 +282,7 @@ class DownloadCache(
|
|||||||
),
|
),
|
||||||
] ?: return
|
] ?: return
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||||
if (it in mangaDir.chapterDirs) {
|
if (it in mangaDir.chapterDirs) {
|
||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ class DownloadManager(
|
|||||||
val chapterDir = provider.findChapterDir(
|
val chapterDir = provider.findChapterDir(
|
||||||
chapter.name,
|
chapter.name,
|
||||||
chapter.scanlator,
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
/* SY --> */ manga.ogTitle /* SY <-- */,
|
/* SY --> */ manga.ogTitle /* SY <-- */,
|
||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
@@ -195,11 +196,12 @@ class DownloadManager(
|
|||||||
fun isChapterDownloaded(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
chapterScanlator: String?,
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean = false,
|
skipCache: Boolean = false,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
|
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -440,7 +442,7 @@ class DownloadManager(
|
|||||||
* @param newChapter the target chapter with the new name.
|
* @param newChapter the target chapter with the new name.
|
||||||
*/
|
*/
|
||||||
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, oldChapter.url)
|
||||||
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
|
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
|
||||||
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
||||||
return
|
return
|
||||||
@@ -451,7 +453,7 @@ class DownloadManager(
|
|||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull() ?: return
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
|
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
||||||
newName += ".cbz"
|
newName += ".cbz"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash.md5
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.storage.displayablePath
|
import tachiyomi.core.common.storage.displayablePath
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -25,6 +27,7 @@ import java.io.IOException
|
|||||||
class DownloadProvider(
|
class DownloadProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storageManager: StorageManager = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val downloadsDir: UniFile?
|
private val downloadsDir: UniFile?
|
||||||
@@ -96,9 +99,15 @@ 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 chapter.
|
* @param source the source of the chapter.
|
||||||
*/
|
*/
|
||||||
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
fun findChapterDir(
|
||||||
|
chapterName: String,
|
||||||
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
mangaTitle: String,
|
||||||
|
source: Source,
|
||||||
|
): UniFile? {
|
||||||
val mangaDir = findMangaDir(mangaTitle, source)
|
val mangaDir = findMangaDir(mangaTitle, source)
|
||||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -113,7 +122,7 @@ class DownloadProvider(
|
|||||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
||||||
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
|
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
|
||||||
return mangaDir to chapters.mapNotNull { chapter ->
|
return mangaDir to chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
@@ -136,7 +145,7 @@ class DownloadProvider(
|
|||||||
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return emptyList()
|
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return emptyList()
|
||||||
return mangaDir.listFiles().orEmpty().asList().filter {
|
return mangaDir.listFiles().orEmpty().asList().filter {
|
||||||
chapters.find { chp ->
|
chapters.find { chp ->
|
||||||
getValidChapterDirNames(chp.name, chp.scanlator).any { dir ->
|
getValidChapterDirNames(chp.name, chp.scanlator, chp.url).any { dir ->
|
||||||
mangaDir.findFile(dir) != null
|
mangaDir.findFile(dir) != null
|
||||||
}
|
}
|
||||||
} == null ||
|
} == null ||
|
||||||
@@ -151,7 +160,10 @@ class DownloadProvider(
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return DiskUtil.buildValidFilename(source.toString())
|
return DiskUtil.buildValidFilename(
|
||||||
|
source.toString(),
|
||||||
|
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,23 +172,75 @@ class DownloadProvider(
|
|||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
*/
|
*/
|
||||||
fun getMangaDirName(mangaTitle: String): String {
|
fun getMangaDirName(mangaTitle: String): String {
|
||||||
return DiskUtil.buildValidFilename(mangaTitle)
|
return DiskUtil.buildValidFilename(
|
||||||
|
mangaTitle,
|
||||||
|
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the chapter directory name for a chapter.
|
* Returns the chapter directory name for a chapter.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapterName the name of the chapter to query.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
* @param chapterScanlator scanlator of the chapter to query.
|
||||||
|
* @param chapterUrl url of the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
|
fun getChapterDirName(
|
||||||
val newChapterName = sanitizeChapterName(chapterName)
|
chapterName: String,
|
||||||
return DiskUtil.buildValidFilename(
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
): String {
|
||||||
|
var dirName = sanitizeChapterName(chapterName)
|
||||||
|
if (!chapterScanlator.isNullOrBlank()) {
|
||||||
|
dirName = chapterScanlator + "_" + dirName
|
||||||
|
}
|
||||||
|
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
|
||||||
|
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
|
||||||
|
dirName += "_" + md5(chapterUrl).take(6)
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of names that might have been previously used as
|
||||||
|
* the directory name for a chapter.
|
||||||
|
* Add to this list if naming pattern ever changes.
|
||||||
|
*
|
||||||
|
* @param chapterName the name of the chapter to query.
|
||||||
|
* @param chapterScanlator scanlator of the chapter to query.
|
||||||
|
* @param chapterUrl url of the chapter to query.
|
||||||
|
*/
|
||||||
|
private fun getLegacyChapterDirNames(
|
||||||
|
chapterName: String,
|
||||||
|
chapterScanlator: String?,
|
||||||
|
chapterUrl: String,
|
||||||
|
): List<String> {
|
||||||
|
val sanitizedChapterName = sanitizeChapterName(chapterName)
|
||||||
|
val chapterNameV1 = DiskUtil.buildValidFilename(
|
||||||
when {
|
when {
|
||||||
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
|
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
|
||||||
else -> newChapterName
|
else -> sanitizedChapterName
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get the filename that would be generated if the user were
|
||||||
|
// using the other value for the disallow non-ASCII
|
||||||
|
// filenames setting. This ensures that chapters downloaded
|
||||||
|
// before the user changed the setting can still be found.
|
||||||
|
val otherChapterDirName =
|
||||||
|
getChapterDirName(
|
||||||
|
chapterName,
|
||||||
|
chapterScanlator,
|
||||||
|
chapterUrl,
|
||||||
|
!libraryPreferences.disallowNonAsciiFilenames().get(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return buildList(2) {
|
||||||
|
// Chapter name without hash (unable to handle duplicate
|
||||||
|
// chapter names)
|
||||||
|
add(chapterNameV1)
|
||||||
|
add(otherChapterDirName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,22 +255,22 @@ class DownloadProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
||||||
return oldChapter.name != newChapter.name ||
|
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
|
||||||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
|
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns valid downloaded chapter directory names.
|
* Returns valid downloaded chapter directory names.
|
||||||
*
|
*
|
||||||
* @param chapterName the name of the chapter to query.
|
* @param chapter the domain chapter object.
|
||||||
* @param chapterScanlator scanlator of the chapter to query
|
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
|
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
|
||||||
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
|
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
|
||||||
return buildList(4) {
|
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
|
||||||
|
|
||||||
|
return buildList {
|
||||||
// Folder of images
|
// Folder of images
|
||||||
add(chapterDirName)
|
add(chapterDirName)
|
||||||
|
|
||||||
// Archived chapters
|
// Archived chapters
|
||||||
add("$chapterDirName.cbz")
|
add("$chapterDirName.cbz")
|
||||||
|
|
||||||
@@ -218,6 +282,12 @@ class DownloadProvider(
|
|||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
add(DiskUtil.buildValidFilename(chapterName))
|
add(DiskUtil.buildValidFilename(chapterName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// any legacy names
|
||||||
|
legacyChapterDirNames.forEach {
|
||||||
|
add(it)
|
||||||
|
add("$it.cbz")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
|||||||
@@ -201,15 +201,17 @@ class Downloader(
|
|||||||
if (isRunning) return
|
if (isRunning) return
|
||||||
|
|
||||||
downloaderJob = scope.launch {
|
downloaderJob = scope.launch {
|
||||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
val activeDownloadsFlow = combine(
|
||||||
|
queueState,
|
||||||
|
downloadPreferences.parallelSourceLimit().changes(),
|
||||||
|
) { a, b -> a to b }.transformLatest { (queue, parallelCount) ->
|
||||||
while (true) {
|
while (true) {
|
||||||
val activeDownloads = queue.asSequence()
|
val activeDownloads = queue.asSequence()
|
||||||
// Ignore completed downloads, leave them in the queue
|
// Ignore completed downloads, leave them in the queue
|
||||||
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
||||||
.groupBy { it.source }
|
.groupBy { it.source }
|
||||||
.toList()
|
.toList()
|
||||||
// Concurrently download from 5 different sources
|
.take(parallelCount)
|
||||||
.take(5)
|
|
||||||
.map { (_, downloads) -> downloads.first() }
|
.map { (_, downloads) -> downloads.first() }
|
||||||
emit(activeDownloads)
|
emit(activeDownloads)
|
||||||
|
|
||||||
@@ -221,7 +223,8 @@ class Downloader(
|
|||||||
}.filter { it }
|
}.filter { it }
|
||||||
activeDownloadsErroredFlow.first()
|
activeDownloadsErroredFlow.first()
|
||||||
}
|
}
|
||||||
}.distinctUntilChanged()
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
@@ -285,7 +288,13 @@ class Downloader(
|
|||||||
val chaptersToQueue = chapters.asSequence()
|
val chaptersToQueue = chapters.asSequence()
|
||||||
// Filter out those already downloaded.
|
// Filter out those already downloaded.
|
||||||
.filter {
|
.filter {
|
||||||
provider.findChapterDir(it.name, it.scanlator, /* SY --> */ manga.ogTitle /* SY <-- */, source) == null
|
provider.findChapterDir(
|
||||||
|
it.name,
|
||||||
|
it.scanlator,
|
||||||
|
it.url,
|
||||||
|
/* SY --> */ manga.ogTitle, /* SY <-- */
|
||||||
|
source,
|
||||||
|
) == null
|
||||||
}
|
}
|
||||||
// Add chapters to queue from the start.
|
// Add chapters to queue from the start.
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
@@ -311,7 +320,10 @@ class Downloader(
|
|||||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||||
) {
|
) {
|
||||||
notifier.onWarning(
|
notifier.onWarning(
|
||||||
context.stringResource(MR.strings.download_queue_size_warning),
|
context.stringResource(
|
||||||
|
MR.strings.download_queue_size_warning,
|
||||||
|
context.stringResource(MR.strings.app_name),
|
||||||
|
),
|
||||||
WARNING_NOTIF_TIMEOUT_MS,
|
WARNING_NOTIF_TIMEOUT_MS,
|
||||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||||
)
|
)
|
||||||
@@ -327,11 +339,12 @@ 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 =
|
||||||
download.status = Download.State.ERROR
|
provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e ->
|
||||||
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
|
download.status = Download.State.ERROR
|
||||||
return
|
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) {
|
||||||
@@ -345,7 +358,11 @@ class Downloader(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
|
val chapterDirname = provider.getChapterDirName(
|
||||||
|
download.chapter.name,
|
||||||
|
download.chapter.scanlator,
|
||||||
|
download.chapter.url,
|
||||||
|
)
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -377,24 +394,23 @@ class Downloader(
|
|||||||
download.status = Download.State.DOWNLOADING
|
download.status = Download.State.DOWNLOADING
|
||||||
|
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// Concurrently do 2 pages at a time
|
pageList.asFlow().flatMapMerge(concurrency = downloadPreferences.parallelPageLimit().get()) { page ->
|
||||||
pageList.asFlow()
|
flow {
|
||||||
.flatMapMerge(concurrency = 2) { page ->
|
// Fetch image URL if necessary
|
||||||
flow {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
// Fetch image URL if necessary
|
page.status = Page.State.LoadPage
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
try {
|
||||||
page.status = Page.State.LoadPage
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
try {
|
} catch (e: Throwable) {
|
||||||
page.imageUrl = download.source.getImageUrl(page)
|
page.status = Page.State.Error(e)
|
||||||
} catch (e: Throwable) {
|
|
||||||
page.status = Page.State.Error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
withIOContext { getOrDownloadImage(page, download, tmpDir, dataSaver) }
|
withIOContext { getOrDownloadImage(page, download, tmpDir, dataSaver) }
|
||||||
emit(page)
|
emit(page)
|
||||||
}.flowOn(Dispatchers.IO)
|
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
.collect {
|
.collect {
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
notifier.onProgressChange(download)
|
notifier.onProgressChange(download)
|
||||||
@@ -465,6 +481,7 @@ class Downloader(
|
|||||||
imageFile != null -> imageFile
|
imageFile != null -> imageFile
|
||||||
chapterCache.isImageInCache(page.imageUrl!!) ->
|
chapterCache.isImageInCache(page.imageUrl!!) ->
|
||||||
copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
||||||
|
|
||||||
else -> downloadImage(page, download.source, tmpDir, filename, dataSaver)
|
else -> downloadImage(page, download.source, tmpDir, filename, dataSaver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.work.WorkInfo
|
|||||||
import androidx.work.WorkQuery
|
import androidx.work.WorkQuery
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
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
|
||||||
@@ -66,7 +65,6 @@ 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.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.library.model.GroupLibraryMode
|
import tachiyomi.domain.library.model.GroupLibraryMode
|
||||||
@@ -130,8 +128,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
private val insertTrack: InsertTrack = Injekt.get()
|
private val insertTrack: InsertTrack = Injekt.get()
|
||||||
private val trackerManager: TrackerManager = Injekt.get()
|
private val trackerManager: TrackerManager = Injekt.get()
|
||||||
private val mdList = trackerManager.mdList
|
private val mdList = trackerManager.mdList
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get()
|
|
||||||
private val setReadStatus: SetReadStatus = Injekt.get()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private val notifier = LibraryUpdateNotifier(context)
|
private val notifier = LibraryUpdateNotifier(context)
|
||||||
@@ -156,7 +152,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } ?: Target.CHAPTERS
|
val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) }
|
||||||
|
?: Target.CHAPTERS
|
||||||
|
|
||||||
// If this is a chapter update, set the last update time to now
|
// If this is a chapter update, set the last update time to now
|
||||||
if (target == Target.CHAPTERS) {
|
if (target == Target.CHAPTERS) {
|
||||||
@@ -220,28 +217,23 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val listToUpdate = if (categoryId != -1L) {
|
val listToUpdate = if (categoryId != -1L) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { categoryId in it.categories }
|
||||||
|
// SY -->
|
||||||
} else if (
|
} else if (
|
||||||
group == LibraryGroup.BY_DEFAULT ||
|
group == LibraryGroup.BY_DEFAULT ||
|
||||||
groupLibraryUpdateType == GroupLibraryMode.GLOBAL ||
|
groupLibraryUpdateType == GroupLibraryMode.GLOBAL ||
|
||||||
(groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)
|
(groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)
|
||||||
) {
|
) {
|
||||||
val categoriesToUpdate = libraryPreferences.updateCategories().get().map(String::toLong)
|
// SY <--
|
||||||
val includedManga = if (categoriesToUpdate.isNotEmpty()) {
|
val includedCategories = libraryPreferences.updateCategories().get().map { it.toLong() }.toSet()
|
||||||
libraryManga.filter { it.category in categoriesToUpdate }
|
val excludedCategories = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }.toSet()
|
||||||
} else {
|
|
||||||
libraryManga
|
|
||||||
}
|
|
||||||
|
|
||||||
val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }
|
libraryManga.filter {
|
||||||
val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
|
val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty()
|
||||||
libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
|
val excluded = it.categories.intersect(excludedCategories).isNotEmpty()
|
||||||
} else {
|
included && !excluded
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
|
// SY -->
|
||||||
includedManga
|
|
||||||
.filterNot { it.manga.id in excludedMangaIds }
|
|
||||||
} else {
|
} else {
|
||||||
when (group) {
|
when (group) {
|
||||||
LibraryGroup.BY_TRACK_STATUS -> {
|
LibraryGroup.BY_TRACK_STATUS -> {
|
||||||
@@ -255,6 +247,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
status.int == trackingExtra
|
status.int == trackingExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
val sourceExtra = groupExtra?.nullIfBlank()?.toIntOrNull()
|
val sourceExtra = groupExtra?.nullIfBlank()?.toIntOrNull()
|
||||||
val source = libraryManga.map { it.manga.source }
|
val source = libraryManga.map { it.manga.source }
|
||||||
@@ -264,12 +257,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
if (source != null) libraryManga.filter { it.manga.source == source } else emptyList()
|
if (source != null) libraryManga.filter { it.manga.source == source } else emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryGroup.BY_STATUS -> {
|
LibraryGroup.BY_STATUS -> {
|
||||||
val statusExtra = groupExtra?.toLongOrNull() ?: -1
|
val statusExtra = groupExtra?.toLongOrNull() ?: -1
|
||||||
libraryManga.filter {
|
libraryManga.filter {
|
||||||
it.manga.status == statusExtra
|
it.manga.status == statusExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryGroup.UNGROUPED -> libraryManga
|
LibraryGroup.UNGROUPED -> libraryManga
|
||||||
else -> libraryManga
|
else -> libraryManga
|
||||||
}
|
}
|
||||||
@@ -286,10 +281,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
// SY <--
|
// SY <--
|
||||||
.filter {
|
.filter {
|
||||||
when {
|
when {
|
||||||
it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> {
|
it.manga.updateStrategy == UpdateStrategy.ONLY_FETCH_ONCE && it.totalChapters > 0L -> {
|
||||||
skippedUpdates.add(
|
skippedUpdates.add(
|
||||||
it.manga to
|
it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update),
|
||||||
context.stringResource(MR.strings.skipped_reason_not_always_update),
|
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -311,11 +305,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> {
|
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> {
|
||||||
skippedUpdates.add(
|
skippedUpdates.add(
|
||||||
it.manga to
|
it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period),
|
||||||
context.stringResource(MR.strings.skipped_reason_not_in_release_period),
|
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,9 +322,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
logcat {
|
logcat {
|
||||||
skippedUpdates
|
skippedUpdates
|
||||||
.groupBy { it.second }
|
.groupBy { it.second }
|
||||||
.map { (reason, entries) ->
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
"$reason: [${entries.map { it.first.title }.sorted().joinToString()}]"
|
|
||||||
}
|
|
||||||
.joinToString()
|
.joinToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,13 +413,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
val errorMessage = when (e) {
|
val errorMessage = when (e) {
|
||||||
is NoChaptersException ->
|
is NoChaptersException -> context.stringResource(
|
||||||
context.stringResource(MR.strings.no_chapters_error)
|
MR.strings.no_chapters_error,
|
||||||
// failedUpdates will already have the source,
|
)
|
||||||
// don't need to copy it into the message
|
// failedUpdates will already have the source, don't need to copy it into the message
|
||||||
is SourceNotInstalledException -> context.stringResource(
|
is SourceNotInstalledException -> context.stringResource(
|
||||||
MR.strings.loader_not_implemented_error,
|
MR.strings.loader_not_implemented_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> e.message
|
else -> e.message
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
@@ -539,7 +532,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
.copyFrom(networkManga)
|
.copyFrom(networkManga)
|
||||||
try {
|
try {
|
||||||
updateManga.await(updatedManga.toMangaUpdate())
|
updateManga.await(updatedManga.toMangaUpdate())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
|
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -580,7 +573,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
count++
|
count++
|
||||||
notifier.showProgressNotification(
|
notifier.showProgressNotification(
|
||||||
listOf(Manga.create().copy(ogTitle = networkManga.title)), count, size,
|
listOf(Manga.create().copy(ogTitle = networkManga.title)),
|
||||||
|
count,
|
||||||
|
size,
|
||||||
)
|
)
|
||||||
|
|
||||||
var dbManga = getManga.await(networkManga.url, mangaDex.id)
|
var dbManga = getManga.await(networkManga.url, mangaDex.id)
|
||||||
@@ -697,7 +692,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
return File("")
|
return File("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,8 +718,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
private const val ERROR_LOG_HELP_URL = "https://mihon.app/docs/guides/troubleshooting/"
|
private const val ERROR_LOG_HELP_URL = "https://mihon.app/docs/guides/troubleshooting/"
|
||||||
|
|
||||||
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|staff {
|
|staff {
|
||||||
|edges {
|
|edges {
|
||||||
|role
|
|role
|
||||||
|
|id
|
||||||
|node {
|
|node {
|
||||||
|name {
|
|name {
|
||||||
|userPreferred
|
|userPreferred
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||||
track.remote_id = remoteTrack.remote_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else track.status
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<KitsuAddMangaResult>()
|
.parseAs<KitsuAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.remote_id = it.data.id
|
track.library_id = it.data.id
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
val data = buildJsonObject {
|
val data = buildJsonObject {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
put("id", track.remote_id)
|
put("id", track.library_id)
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toApiStatus())
|
put("status", track.toApiStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
@@ -105,7 +105,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
|
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url("${BASE_URL}library-entries/${track.remote_id}")
|
.url("${BASE_URL}library-entries/${track.library_id}")
|
||||||
.headers(
|
.headers(
|
||||||
headersOf("Content-Type", VND_API_JSON),
|
headersOf("Content-Type", VND_API_JSON),
|
||||||
)
|
)
|
||||||
@@ -122,7 +122,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
"${BASE_URL}library-entries/${track.remoteId}",
|
"${BASE_URL}library-entries/${track.libraryId}",
|
||||||
headers = headersOf("Content-Type", VND_API_JSON),
|
headers = headersOf("Content-Type", VND_API_JSON),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -195,7 +195,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
suspend fun getLibManga(track: Track): Track {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
val url = "${BASE_URL}library-entries".toUri().buildUpon()
|
||||||
.encodedQuery("filter[id]=${track.remote_id}")
|
.encodedQuery("filter[id]=${track.library_id}")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ data class KitsuListSearchResult(
|
|||||||
val manga = included[0].attributes
|
val manga = included[0].attributes
|
||||||
|
|
||||||
return TrackSearch.create(TrackerManager.KITSU).apply {
|
return TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
remote_id = userData.id
|
remote_id = included[0].id
|
||||||
|
library_id = userData.id
|
||||||
title = manga.canonicalTitle
|
title = manga.canonicalTitle
|
||||||
total_chapters = manga.chapterCount ?: 0
|
total_chapters = manga.chapterCount ?: 0
|
||||||
cover_url = manga.posterImage?.original ?: ""
|
cover_url = manga.posterImage?.original ?: ""
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
val remoteTrack = api.getTrackSearch(track.remote_id)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
return track
|
return track
|
||||||
@@ -88,14 +88,13 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
|
|
||||||
override suspend fun match(manga: DomainManga): TrackSearch? =
|
override suspend fun match(manga: DomainManga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
api.getTrackSearch(manga.url)
|
api.getTrackSearch(manga.url.getMangaId())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let {
|
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean =
|
||||||
accept(it)
|
track.remoteUrl == manga.url && source?.let { accept(it) } == true
|
||||||
} == true
|
|
||||||
|
|
||||||
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
|
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
|
||||||
if (accept(newSource)) {
|
if (accept(newSource)) {
|
||||||
@@ -103,4 +102,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.getMangaId(): Long =
|
||||||
|
this.substringAfterLast('/').toLong()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,168 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Credentials
|
import kotlinx.serialization.json.addAll
|
||||||
import okhttp3.Dns
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import okhttp3.FormBody
|
import kotlinx.serialization.json.put
|
||||||
import okhttp3.Headers
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class SuwayomiApi(private val trackId: Long) {
|
class SuwayomiApi(private val trackId: Long) {
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val client: OkHttpClient =
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
network.client.newBuilder()
|
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
|
||||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
private val client: OkHttpClient by lazy { source.client }
|
||||||
.build()
|
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||||
|
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
||||||
|
|
||||||
private fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
|
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||||
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
|
val query = """
|
||||||
val credentials = Credentials.basic(baseLogin, basePassword)
|
|query GetManga(${'$'}mangaId: Int!) {
|
||||||
add("Authorization", credentials)
|
| manga(id: ${'$'}mangaId) {
|
||||||
|
| ...MangaFragment
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
|
|
||||||
|
|$MangaFragment
|
||||||
|
""".trimMargin()
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
put("query", query)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val headers: Headers by lazy { headersBuilder().build() }
|
|
||||||
|
|
||||||
private val baseUrl by lazy { getPrefBaseUrl() }
|
|
||||||
private val baseLogin by lazy { getPrefBaseLogin() }
|
|
||||||
private val basePassword by lazy { getPrefBasePassword() }
|
|
||||||
|
|
||||||
suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext {
|
|
||||||
val url = try {
|
|
||||||
// test if getting api url or manga id
|
|
||||||
val mangaId = trackUrl.toLong()
|
|
||||||
"$baseUrl/api/v1/manga/$mangaId"
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
trackUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
val manga = with(json) {
|
val manga = with(json) {
|
||||||
client.newCall(GET("$url/full", headers))
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MangaDataClass>()
|
.parseAs<GetMangaResult>()
|
||||||
|
.data
|
||||||
|
.entry
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSearch.create(trackId).apply {
|
TrackSearch.create(trackId).apply {
|
||||||
|
remote_id = mangaId
|
||||||
title = manga.title
|
title = manga.title
|
||||||
cover_url = "$url/thumbnail"
|
cover_url = "$baseUrl/${manga.thumbnailUrl}"
|
||||||
summary = manga.description.orEmpty()
|
summary = manga.description.orEmpty()
|
||||||
tracking_url = url
|
tracking_url = "$baseUrl/manga/$mangaId"
|
||||||
total_chapters = manga.chapterCount
|
total_chapters = manga.chapters.totalCount.toLong()
|
||||||
publishing_status = manga.status
|
publishing_status = manga.status.name
|
||||||
last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0.0
|
last_chapter_read = manga.latestReadChapter?.chapterNumber ?: 0.0
|
||||||
status = when (manga.unreadCount) {
|
status = when (manga.unreadCount) {
|
||||||
manga.chapterCount -> Suwayomi.UNREAD
|
manga.chapters.totalCount -> Suwayomi.UNREAD
|
||||||
0L -> Suwayomi.COMPLETED
|
0 -> Suwayomi.COMPLETED
|
||||||
else -> Suwayomi.READING
|
else -> Suwayomi.READING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateProgress(track: Track): Track {
|
suspend fun updateProgress(track: Track): Track {
|
||||||
val url = track.tracking_url
|
val mangaId = track.remote_id
|
||||||
val chapters = with(json) {
|
|
||||||
client.newCall(GET("$url/chapters", headers))
|
val chaptersQuery = """
|
||||||
.awaitSuccess()
|
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
||||||
.parseAs<List<ChapterDataClass>>()
|
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
||||||
|
| nodes {
|
||||||
|
| id
|
||||||
|
| chapterNumber
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val chaptersPayload = buildJsonObject {
|
||||||
|
put("query", chaptersQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chaptersToMark = with(json) {
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = chaptersPayload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<GetMangaUnreadChaptersResult>()
|
||||||
|
.data
|
||||||
|
.entry
|
||||||
|
.nodes
|
||||||
|
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
|
||||||
}
|
}
|
||||||
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
|
|
||||||
|
|
||||||
client.newCall(
|
val markQuery = """
|
||||||
PUT(
|
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||||
"$url/chapter/$lastChapterIndex",
|
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||||
headers,
|
| chapters {
|
||||||
FormBody.Builder(Charset.forName("utf8"))
|
| id
|
||||||
.add("markPrevRead", "true")
|
| }
|
||||||
.add("read", "true")
|
| }
|
||||||
.build(),
|
|}
|
||||||
),
|
""".trimMargin()
|
||||||
).awaitSuccess()
|
val markPayload = buildJsonObject {
|
||||||
|
put("query", markQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
putJsonArray("chapters") {
|
||||||
|
addAll(chaptersToMark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with(json) {
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = markPayload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
return getTrackSearch(track.tracking_url)
|
val trackQuery = """
|
||||||
|
|mutation TrackManga(${'$'}mangaId: Int!) {
|
||||||
|
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
||||||
|
| trackRecords {
|
||||||
|
| lastChapterRead
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val trackPayload = buildJsonObject {
|
||||||
|
put("query", trackQuery)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("mangaId", mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with(json) {
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
apiUrl,
|
||||||
|
body = trackPayload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTrackSearch(track.remote_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sourceId by lazy {
|
private val sourceId by lazy {
|
||||||
@@ -106,18 +171,35 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
companion object {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE)
|
private val MangaFragment = """
|
||||||
|
|fragment MangaFragment on MangaType {
|
||||||
|
| artist
|
||||||
|
| author
|
||||||
|
| description
|
||||||
|
| id
|
||||||
|
| status
|
||||||
|
| thumbnailUrl
|
||||||
|
| title
|
||||||
|
| url
|
||||||
|
| genre
|
||||||
|
| inLibraryAt
|
||||||
|
| chapters {
|
||||||
|
| totalCount
|
||||||
|
| }
|
||||||
|
| latestUploadedChapter {
|
||||||
|
| uploadDate
|
||||||
|
| }
|
||||||
|
| latestFetchedChapter {
|
||||||
|
| fetchedAt
|
||||||
|
| }
|
||||||
|
| latestReadChapter {
|
||||||
|
| lastReadAt
|
||||||
|
| chapterNumber
|
||||||
|
| }
|
||||||
|
| unreadCount
|
||||||
|
| downloadCount
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
|
||||||
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
|
|
||||||
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val ADDRESS_TITLE = "Server URL Address"
|
|
||||||
private const val ADDRESS_DEFAULT = ""
|
|
||||||
private const val LOGIN_TITLE = "Login (Basic Auth)"
|
|
||||||
private const val LOGIN_DEFAULT = ""
|
|
||||||
private const val PASSWORD_TITLE = "Password (Basic Auth)"
|
|
||||||
private const val PASSWORD_DEFAULT = ""
|
|
||||||
|
|||||||
@@ -1,100 +1,90 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
public enum class MangaStatus(
|
||||||
|
public val rawValue: String,
|
||||||
|
) {
|
||||||
|
UNKNOWN("UNKNOWN"),
|
||||||
|
ONGOING("ONGOING"),
|
||||||
|
COMPLETED("COMPLETED"),
|
||||||
|
LICENSED("LICENSED"),
|
||||||
|
PUBLISHING_FINISHED("PUBLISHING_FINISHED"),
|
||||||
|
CANCELLED("CANCELLED"),
|
||||||
|
ON_HIATUS("ON_HIATUS"),
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SourceDataClass(
|
public data class MangaFragment(
|
||||||
val id: String,
|
public val artist: String?,
|
||||||
val name: String,
|
public val author: String?,
|
||||||
val lang: String,
|
public val description: String?,
|
||||||
val iconUrl: String,
|
public val id: Int,
|
||||||
|
public val status: MangaStatus,
|
||||||
|
public val thumbnailUrl: String?,
|
||||||
|
public val title: String,
|
||||||
|
public val url: String,
|
||||||
|
public val genre: List<String>,
|
||||||
|
public val inLibraryAt: Long,
|
||||||
|
public val chapters: Chapters,
|
||||||
|
public val latestUploadedChapter: LatestUploadedChapter?,
|
||||||
|
public val latestFetchedChapter: LatestFetchedChapter?,
|
||||||
|
public val latestReadChapter: LatestReadChapter?,
|
||||||
|
public val unreadCount: Int,
|
||||||
|
public val downloadCount: Int,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
public data class Chapters(
|
||||||
|
public val totalCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source provides a latest listing */
|
@Serializable
|
||||||
val supportsLatest: Boolean,
|
public data class LatestUploadedChapter(
|
||||||
|
public val uploadDate: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source implements [ConfigurableSource] */
|
@Serializable
|
||||||
val isConfigurable: Boolean,
|
public data class LatestFetchedChapter(
|
||||||
|
public val fetchedAt: Long,
|
||||||
|
)
|
||||||
|
|
||||||
/** The Source class has a @Nsfw annotation */
|
@Serializable
|
||||||
val isNsfw: Boolean,
|
public data class LatestReadChapter(
|
||||||
|
public val lastReadAt: Long,
|
||||||
|
public val chapterNumber: Double,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** A nicer version of [name] */
|
@Serializable
|
||||||
val displayName: String,
|
public data class GetMangaResult(
|
||||||
|
public val data: GetMangaData,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaDataClass(
|
public data class GetMangaData(
|
||||||
val id: Int,
|
@SerialName("manga")
|
||||||
val sourceId: String,
|
public val entry: MangaFragment,
|
||||||
|
|
||||||
val url: String,
|
|
||||||
val title: String,
|
|
||||||
val thumbnailUrl: String?,
|
|
||||||
|
|
||||||
val initialized: Boolean,
|
|
||||||
|
|
||||||
val artist: String?,
|
|
||||||
val author: String?,
|
|
||||||
val description: String?,
|
|
||||||
val genre: List<String>,
|
|
||||||
val status: String,
|
|
||||||
val inLibrary: Boolean,
|
|
||||||
val inLibraryAt: Long,
|
|
||||||
val source: SourceDataClass?,
|
|
||||||
|
|
||||||
val meta: Map<String, String>,
|
|
||||||
|
|
||||||
val realUrl: String?,
|
|
||||||
val lastFetchedAt: Long?,
|
|
||||||
val chaptersLastFetchedAt: Long?,
|
|
||||||
|
|
||||||
val freshData: Boolean,
|
|
||||||
val unreadCount: Long?,
|
|
||||||
val downloadCount: Long?,
|
|
||||||
val chapterCount: Long, // actually is nullable server side, but should be set at this time
|
|
||||||
val lastChapterRead: ChapterDataClass?,
|
|
||||||
|
|
||||||
val age: Long?,
|
|
||||||
val chaptersAge: Long?,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterDataClass(
|
public data class GetMangaUnreadChaptersEntry(
|
||||||
val id: Int,
|
public val nodes: List<GetMangaUnreadChaptersNode>,
|
||||||
val url: String,
|
)
|
||||||
val name: String,
|
|
||||||
val uploadDate: Long,
|
@Serializable
|
||||||
val chapterNumber: Double,
|
public data class GetMangaUnreadChaptersNode(
|
||||||
val scanlator: String?,
|
public val id: Int,
|
||||||
val mangaId: Int,
|
public val chapterNumber: Double,
|
||||||
|
)
|
||||||
/** chapter is read */
|
|
||||||
val read: Boolean,
|
@Serializable
|
||||||
|
public data class GetMangaUnreadChaptersResult(
|
||||||
/** chapter is bookmarked */
|
public val data: GetMangaUnreadChaptersData,
|
||||||
val bookmarked: Boolean,
|
)
|
||||||
|
|
||||||
/** last read page, zero means not read/no data */
|
@Serializable
|
||||||
val lastPageRead: Int,
|
public data class GetMangaUnreadChaptersData(
|
||||||
|
@SerialName("chapters")
|
||||||
/** last read page, zero means not read/no data */
|
public val entry: GetMangaUnreadChaptersEntry,
|
||||||
val lastReadAt: Long,
|
|
||||||
|
|
||||||
/** this chapter's index, starts with 1 */
|
|
||||||
val index: Int,
|
|
||||||
|
|
||||||
/** the date we fist saw this chapter*/
|
|
||||||
val fetchedAt: Long,
|
|
||||||
|
|
||||||
/** is chapter downloaded */
|
|
||||||
val downloaded: Boolean,
|
|
||||||
|
|
||||||
/** used to construct pages in the front-end */
|
|
||||||
val pageCount: Int,
|
|
||||||
|
|
||||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
|
||||||
val chapterCount: Int?,
|
|
||||||
|
|
||||||
/** used to store client specific values */
|
|
||||||
val meta: Map<String, String>,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class ExtensionManager(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext { context.toast(MR.strings.extension_api_error) }
|
withUIContext { context.toast(MR.strings.extension_api_error) }
|
||||||
emptyList()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
enableAdditionalSubLanguages(extensions)
|
enableAdditionalSubLanguages(extensions)
|
||||||
|
|||||||
@@ -1,30 +1,73 @@
|
|||||||
package eu.kanade.tachiyomi.extension.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.IBinder
|
||||||
import android.os.Process
|
import androidx.core.content.ContextCompat
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import mihon.app.shizuku.IShellInterface
|
||||||
|
import mihon.app.shizuku.ShellInterface
|
||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
import rikka.shizuku.ShizukuRemoteProcess
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.lang.reflect.Method
|
|
||||||
|
|
||||||
class ShizukuInstaller(private val service: Service) : Installer(service) {
|
class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private var shellInterface: IShellInterface? = null
|
||||||
|
|
||||||
|
private val shizukuArgs by lazy {
|
||||||
|
Shizuku.UserServiceArgs(
|
||||||
|
ComponentName(service, ShellInterface::class.java),
|
||||||
|
)
|
||||||
|
.tag("shizuku_service")
|
||||||
|
.processNameSuffix("shizuku_service")
|
||||||
|
.debuggable(BuildConfig.DEBUG)
|
||||||
|
.daemon(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val connection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
shellInterface = IShellInterface.Stub.asInterface(service)
|
||||||
|
ready = true
|
||||||
|
checkQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
shellInterface = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)
|
||||||
|
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||||
|
|
||||||
|
if (status == PackageInstaller.STATUS_SUCCESS) {
|
||||||
|
continueQueue(InstallStep.Installed)
|
||||||
|
} else {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to install extension $packageName: $message" }
|
||||||
|
continueQueue(InstallStep.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
||||||
logcat { "Shizuku was killed prematurely" }
|
logcat { "Shizuku was killed prematurely" }
|
||||||
service.stopSelf()
|
service.stopSelf()
|
||||||
@@ -34,8 +77,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||||
if (grantResult == PackageManager.PERMISSION_GRANTED) {
|
if (grantResult == PackageManager.PERMISSION_GRANTED) {
|
||||||
ready = true
|
|
||||||
checkQueue()
|
checkQueue()
|
||||||
|
Shizuku.bindUserService(shizukuArgs, connection)
|
||||||
} else {
|
} else {
|
||||||
service.stopSelf()
|
service.stopSelf()
|
||||||
}
|
}
|
||||||
@@ -44,44 +87,34 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initShizuku() {
|
||||||
|
if (ready) return
|
||||||
|
if (!Shizuku.pingBinder()) {
|
||||||
|
logcat(LogPriority.ERROR) { "Shizuku is not ready to use" }
|
||||||
|
service.toast(MR.strings.ext_installer_shizuku_stopped)
|
||||||
|
service.stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Shizuku.bindUserService(shizukuArgs, connection)
|
||||||
|
} else {
|
||||||
|
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
|
||||||
|
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var ready = false
|
override var ready = false
|
||||||
|
|
||||||
override fun processEntry(entry: Entry) {
|
override fun processEntry(entry: Entry) {
|
||||||
super.processEntry(entry)
|
super.processEntry(entry)
|
||||||
scope.launch {
|
try {
|
||||||
var sessionId: String? = null
|
shellInterface?.install(
|
||||||
try {
|
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
||||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
)
|
||||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
} catch (e: Exception) {
|
||||||
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
val userId = Process.myUserHandle().hashCode()
|
continueQueue(InstallStep.Error)
|
||||||
"pm install-create --user $userId -r -i ${service.packageName} -S $size"
|
|
||||||
} else {
|
|
||||||
"pm install-create -r -i ${service.packageName} -S $size"
|
|
||||||
}
|
|
||||||
val createResult = exec(createCommand)
|
|
||||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
|
||||||
?: throw RuntimeException("Failed to create install session")
|
|
||||||
|
|
||||||
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
|
|
||||||
if (writeResult.resultCode != 0) {
|
|
||||||
throw RuntimeException("Failed to write APK to session $sessionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
val commitResult = exec("pm install-commit $sessionId")
|
|
||||||
if (commitResult.resultCode != 0) {
|
|
||||||
throw RuntimeException("Failed to commit install session $sessionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
continueQueue(InstallStep.Installed)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
|
||||||
if (sessionId != null) {
|
|
||||||
exec("pm install-abandon $sessionId")
|
|
||||||
}
|
|
||||||
continueQueue(InstallStep.Error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,44 +124,26 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||||
|
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||||
|
service.unregisterReceiver(receiver)
|
||||||
|
logcat { "ShizukuInstaller destroy" }
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val newProcess: Method
|
|
||||||
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
|
|
||||||
val process = newProcess.invoke(null, arrayOf("sh", "-c", command), null, null) as ShizukuRemoteProcess
|
|
||||||
if (stdin != null) {
|
|
||||||
process.outputStream.use { stdin.copyTo(it) }
|
|
||||||
}
|
|
||||||
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
|
|
||||||
val resultCode = process.waitFor()
|
|
||||||
return ShellResult(resultCode, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ShellResult(val resultCode: Int, val out: String)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Shizuku.addBinderDeadListener(shizukuDeadListener)
|
Shizuku.addBinderDeadListener(shizukuDeadListener)
|
||||||
ready = if (Shizuku.pingBinder()) {
|
|
||||||
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
ContextCompat.registerReceiver(
|
||||||
true
|
service,
|
||||||
} else {
|
receiver,
|
||||||
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
|
IntentFilter(ACTION_INSTALL_RESULT),
|
||||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
ContextCompat.RECEIVER_EXPORTED,
|
||||||
false
|
)
|
||||||
}
|
|
||||||
} else {
|
initShizuku()
|
||||||
logcat(LogPriority.ERROR) { "Shizuku is not ready to use" }
|
|
||||||
service.toast(MR.strings.ext_installer_shizuku_stopped)
|
|
||||||
service.stopSelf()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
newProcess = Shizuku::class.java
|
|
||||||
.getDeclaredMethod("newProcess", Array<out String>::class.java, Array<out String>::class.java, String::class.java)
|
|
||||||
newProcess.isAccessible = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
|
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
|
||||||
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
|
const val ACTION_INSTALL_RESULT = "${BuildConfig.APPLICATION_ID}.ACTION_INSTALL_RESULT"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.Lanraragi
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
@@ -277,6 +278,13 @@ class AndroidSourceManager(
|
|||||||
NHentai::class,
|
NHentai::class,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"LANraragi",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.lanraragi.LANraragi",
|
||||||
|
Lanraragi::class,
|
||||||
|
true,
|
||||||
|
),
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
val currentDelegatedSources: MutableMap<Long, DelegatedSource> =
|
val currentDelegatedSources: MutableMap<Long, DelegatedSource> =
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
fun Source.getNameForMangaInfo(
|
fun Source.getNameForMangaInfo(
|
||||||
// SY -->
|
// SY -->
|
||||||
mergeSources: List<Source>?,
|
mergeSources: List<Source>? = null,
|
||||||
enabledLanguages: List<String> = Injekt.get<SourcePreferences>().enabledLanguages().get()
|
enabledLanguages: List<String> = Injekt.get<SourcePreferences>().enabledLanguages().get()
|
||||||
.filterNot { it in listOf("all", "other") },
|
.filterNot { it in listOf("all", "other") },
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewInfo
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewPage
|
||||||
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||||
|
import exh.metadata.MetadataUtil
|
||||||
|
import exh.metadata.metadata.LanraragiSearchMetadata
|
||||||
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class Lanraragi(delegate: HttpSource, val context: Context) :
|
||||||
|
DelegatedHttpSource(delegate),
|
||||||
|
MetadataSource<LanraragiSearchMetadata, Response>,
|
||||||
|
NamespaceSource,
|
||||||
|
PagePreviewSource {
|
||||||
|
override val metaClass = LanraragiSearchMetadata::class
|
||||||
|
override fun newMetaInstance() = LanraragiSearchMetadata()
|
||||||
|
override val lang = delegate.lang
|
||||||
|
|
||||||
|
private fun getApiUriBuilder(path: String): Uri.Builder {
|
||||||
|
return LanraragiSearchMetadata.getApiUriBuilder(baseUrl, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getReaderId(url: String): String {
|
||||||
|
return READER_ID_REGEX.find(url)?.groupValues?.get(1) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getThumbnailId(url: String): String {
|
||||||
|
return THUMBNAIL_ID_REGEX.find(url)?.groupValues?.get(1) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
private suspend fun getRandomID(query: String): String {
|
||||||
|
val searchRandom = client.newCall(GET("$baseUrl/api/search/random?count=1&$query", headers)).awaitSuccess()
|
||||||
|
val data = jsonParser.parseToJsonElement(searchRandom.body.string()).jsonObject["data"]
|
||||||
|
val archive = data!!.jsonArray.firstOrNull()?.jsonObject
|
||||||
|
|
||||||
|
// 0.8.2~0.8.7 = id, 0.8.8+ = arcid
|
||||||
|
return (archive?.get("arcid") ?: archive?.get("id"))?.jsonPrimitive?.content ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun customMangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val id = if (manga.url.startsWith("/api/search/random")) {
|
||||||
|
getRandomID(Uri.parse(manga.url).encodedQuery.toString())
|
||||||
|
} else {
|
||||||
|
getReaderId(manga.url)
|
||||||
|
}
|
||||||
|
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
||||||
|
|
||||||
|
return GET(uri.toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||||
|
val response = client.newCall(customMangaDetailsRequest(manga)).awaitSuccess()
|
||||||
|
return parseToManga(manga, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun parseIntoMetadata(metadata: LanraragiSearchMetadata, input: Response) {
|
||||||
|
val archive = with(jsonParser) { input.parseAs<Archive>() }
|
||||||
|
|
||||||
|
with(metadata) {
|
||||||
|
arcId = archive.arcid
|
||||||
|
|
||||||
|
title = archive.title
|
||||||
|
|
||||||
|
summary = archive.summary
|
||||||
|
|
||||||
|
tags.clear()
|
||||||
|
archive.tags?.split(',')
|
||||||
|
?.mapTo(tags) {
|
||||||
|
val tag = it.trim()
|
||||||
|
if (
|
||||||
|
tag.startsWith(LanraragiSearchMetadata.LANRARAGI_NAMESPACE_DATE_ADDED) ||
|
||||||
|
tag.startsWith(LanraragiSearchMetadata.LANRARAGI_NAMESPACE_TIMESTAMP)
|
||||||
|
) {
|
||||||
|
val second = tag.substringAfter(':').trim().toLongOrNull()
|
||||||
|
if (second != null) {
|
||||||
|
val formattedTag = MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||||
|
.format(Instant.ofEpochSecond(second))
|
||||||
|
RaisedTag(
|
||||||
|
tag.substringBefore(':'),
|
||||||
|
formattedTag,
|
||||||
|
LanraragiSearchMetadata.TAG_TYPE_DEFAULT,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RaisedTag(
|
||||||
|
tag.substringBefore(':'),
|
||||||
|
tag.substringAfter(':'),
|
||||||
|
LanraragiSearchMetadata.TAG_TYPE_DEFAULT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RaisedTag(
|
||||||
|
tag.substringBefore(':', LanraragiSearchMetadata.LANRARAGI_NAMESPACE_OTHER),
|
||||||
|
tag.substringAfter(':'),
|
||||||
|
LanraragiSearchMetadata.TAG_TYPE_DEFAULT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCount = archive.pagecount
|
||||||
|
|
||||||
|
filename = archive.filename
|
||||||
|
|
||||||
|
extension = archive.extension
|
||||||
|
|
||||||
|
baseUrl = this@Lanraragi.baseUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Archive(
|
||||||
|
val arcid: String,
|
||||||
|
val isnew: String,
|
||||||
|
val tags: String?,
|
||||||
|
val summary: String?,
|
||||||
|
val title: String,
|
||||||
|
val pagecount: Int,
|
||||||
|
val filename: String,
|
||||||
|
val extension: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getPagePreviewList(manga: SManga, chapters: List<SChapter>, page: Int): PagePreviewPage {
|
||||||
|
val metadata = fetchOrLoadMetadata(manga.id()) {
|
||||||
|
client.newCall(customMangaDetailsRequest(manga)).awaitSuccess()
|
||||||
|
}
|
||||||
|
return PagePreviewPage(
|
||||||
|
page,
|
||||||
|
(1..(metadata.pageCount ?: 1)).map { index ->
|
||||||
|
PagePreviewInfo(
|
||||||
|
index,
|
||||||
|
imageUrl = LanraragiSearchMetadata.getThumbnailUri(baseUrl, metadata.arcId!!, index),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
|
||||||
|
return client.newCachelessCallWithProgress(
|
||||||
|
if (cacheControl != null) {
|
||||||
|
GET(page.imageUrl, cache = cacheControl, headers = headers)
|
||||||
|
} else {
|
||||||
|
GET(page.imageUrl, headers = headers)
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
).awaitSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThumbnailTask(
|
||||||
|
val job: Int,
|
||||||
|
val operation: String,
|
||||||
|
val success: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskProgress(
|
||||||
|
val state: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun minionJobDone(jobId: Int): Boolean {
|
||||||
|
return client.newCall(
|
||||||
|
GET(
|
||||||
|
getApiUriBuilder("/api/minion/$jobId").build().toString(),
|
||||||
|
headers = headers,
|
||||||
|
),
|
||||||
|
).awaitSuccess().let {
|
||||||
|
with(jsonParser) {
|
||||||
|
it.parseAs<TaskProgress>().state == "finished"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl?): Response {
|
||||||
|
return requestPreviewImage(page, cacheControl).let {
|
||||||
|
if (it.code == 202) {
|
||||||
|
val task = with(jsonParser) {
|
||||||
|
it.parseAs<ThumbnailTask>()
|
||||||
|
}
|
||||||
|
var tries = 0
|
||||||
|
do {
|
||||||
|
if (tries > 1) {
|
||||||
|
delay(200.milliseconds)
|
||||||
|
}
|
||||||
|
val jobDone = minionJobDone(task.job)
|
||||||
|
} while (!jobDone && tries++ < 3)
|
||||||
|
requestPreviewImage(page, cacheControl).apply {
|
||||||
|
if (code == 202) {
|
||||||
|
throw IOException("Thumbnail not ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val jsonParser = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val READER_ID_REGEX = Regex("""/reader\?id=(\w{40})""")
|
||||||
|
private val THUMBNAIL_ID_REGEX = Regex("""/(\w{40})/thumbnail""")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ class ThemingDelegateImpl : ThemingDelegate {
|
|||||||
|
|
||||||
private val themeResources: Map<AppTheme, Int> = mapOf(
|
private val themeResources: Map<AppTheme, Int> = mapOf(
|
||||||
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
|
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
|
||||||
|
AppTheme.CATPPUCCIN to R.style.Theme_Tachiyomi_Catppuccin,
|
||||||
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
|
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
|
||||||
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
|
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
|
||||||
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
|
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ class ExtensionsScreenModel(
|
|||||||
|
|
||||||
fun cancelInstallUpdateExtension(extension: Extension) {
|
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||||
extensionManager.cancelInstallUpdateExtension(extension)
|
extensionManager.cancelInstallUpdateExtension(extension)
|
||||||
|
removeDownloadState(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addDownloadState(extension: Extension, installStep: InstallStep) {
|
private fun addDownloadState(extension: Extension, installStep: InstallStep) {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration
|
|
||||||
|
|
||||||
object MigrationFlags {
|
|
||||||
|
|
||||||
const val CHAPTERS = 0b00001
|
|
||||||
const val CATEGORIES = 0b00010
|
|
||||||
const val TRACK = 0b00100
|
|
||||||
const val CUSTOM_COVER = 0b01000
|
|
||||||
const val EXTRA = 0b10000
|
|
||||||
const val DELETE_CHAPTERS = 0b100000
|
|
||||||
const val NOTES = 0b1000000
|
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
|
||||||
return value and CHAPTERS != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCategories(value: Int): Boolean {
|
|
||||||
return value and CATEGORIES != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasTracks(value: Int): Boolean {
|
|
||||||
return value and TRACK != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCustomCover(value: Int): Boolean {
|
|
||||||
return value and CUSTOM_COVER != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasExtra(value: Int): Boolean {
|
|
||||||
return value and EXTRA != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasDeleteChapters(value: Int): Boolean {
|
|
||||||
return value and DELETE_CHAPTERS != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasNotes(value: Int): Boolean {
|
|
||||||
return value and NOTES != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-135
@@ -1,135 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.RadioButton
|
|
||||||
import android.widget.RadioGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import tachiyomi.core.common.preference.Preference
|
|
||||||
import tachiyomi.core.common.util.lang.toLong
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrationBottomSheetDialog(
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onStartMigration: (extraParam: String?) -> Unit,
|
|
||||||
) {
|
|
||||||
val startMigration = rememberUpdatedState(onStartMigration)
|
|
||||||
val state = remember {
|
|
||||||
MigrationBottomSheetDialogState(startMigration)
|
|
||||||
}
|
|
||||||
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
|
||||||
AndroidView(
|
|
||||||
factory = { factoryContext ->
|
|
||||||
val binding = MigrationBottomSheetBinding.inflate(LayoutInflater.from(factoryContext))
|
|
||||||
state.initPreferences(binding)
|
|
||||||
binding.root
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MigrationBottomSheetDialogState(private val onStartMigration: State<(extraParam: String?) -> Unit>) {
|
|
||||||
private val preferences: SourcePreferences by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init general reader preferences.
|
|
||||||
*/
|
|
||||||
fun initPreferences(binding: MigrationBottomSheetBinding) {
|
|
||||||
val flags = preferences.migrateFlags().get()
|
|
||||||
|
|
||||||
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
|
||||||
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
|
||||||
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
|
||||||
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
|
||||||
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
|
||||||
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
|
|
||||||
binding.migNotes.isChecked = MigrationFlags.hasNotes(flags)
|
|
||||||
|
|
||||||
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
|
|
||||||
|
|
||||||
binding.useSmartSearch.bindToPreference(preferences.smartMigration())
|
|
||||||
binding.extraSearchParamText.isVisible = false
|
|
||||||
binding.extraSearchParam.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
binding.extraSearchParamText.isVisible = isChecked
|
|
||||||
}
|
|
||||||
binding.sourceGroup.bindToPreference(preferences.useSourceWithMost())
|
|
||||||
|
|
||||||
binding.skipStep.isChecked = preferences.skipPreMigration().get()
|
|
||||||
binding.HideNotFoundManga.isChecked = preferences.hideNotFoundMigration().get()
|
|
||||||
binding.OnlyShowUpdates.isChecked = preferences.showOnlyUpdatesMigration().get()
|
|
||||||
binding.skipStep.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
binding.root.context.toast(
|
|
||||||
SYMR.strings.pre_migration_skip_toast,
|
|
||||||
Toast.LENGTH_LONG,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.migrateBtn.setOnClickListener {
|
|
||||||
preferences.skipPreMigration().set(binding.skipStep.isChecked)
|
|
||||||
preferences.hideNotFoundMigration().set(binding.HideNotFoundManga.isChecked)
|
|
||||||
preferences.showOnlyUpdatesMigration().set(binding.OnlyShowUpdates.isChecked)
|
|
||||||
onStartMigration.value(
|
|
||||||
if (binding.useSmartSearch.isChecked && binding.extraSearchParamText.text.isNotBlank()) {
|
|
||||||
binding.extraSearchParamText.toString()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFlags(binding: MigrationBottomSheetBinding) {
|
|
||||||
var flags = 0
|
|
||||||
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
|
||||||
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
|
||||||
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
|
||||||
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
|
||||||
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
|
||||||
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
|
|
||||||
if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES
|
|
||||||
preferences.migrateFlags().set(flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a checkbox or switch view with a boolean preference.
|
|
||||||
*/
|
|
||||||
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
|
|
||||||
isChecked = pref.get()
|
|
||||||
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a radio group with a boolean preference.
|
|
||||||
*/
|
|
||||||
private fun RadioGroup.bindToPreference(pref: Preference<Boolean>) {
|
|
||||||
(getChildAt(pref.get().toLong().toInt()) as RadioButton).isChecked = true
|
|
||||||
setOnCheckedChangeListener { _, value ->
|
|
||||||
val index = indexOfChild(findViewById(value))
|
|
||||||
pref.set(index == 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
|
||||||
import androidx.compose.material.icons.outlined.Deselect
|
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
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.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
|
||||||
import eu.kanade.presentation.util.Screen
|
|
||||||
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import java.io.Serializable
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
sealed class MigrationType : Serializable {
|
|
||||||
data class MangaList(val mangaIds: List<Long>) : MigrationType()
|
|
||||||
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreMigrationScreen(val migration: MigrationType) : Screen() {
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
var fabExpanded by remember { mutableStateOf(true) }
|
|
||||||
val items by screenModel.state.collectAsState()
|
|
||||||
val adapter by screenModel.adapter.collectAsState()
|
|
||||||
LaunchedEffect(items.isNotEmpty(), adapter != null) {
|
|
||||||
if (adapter != null && items.isNotEmpty()) {
|
|
||||||
adapter?.updateDataSet(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nestedScrollConnection = remember {
|
|
||||||
// All this lines just for fab state :/
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
fabExpanded = available.y >= 0
|
|
||||||
return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
|
||||||
return scrollBehavior.nestedScrollConnection.onPreFling(available)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppBar(
|
|
||||||
title = stringResource(SYMR.strings.select_sources),
|
|
||||||
navigateUp = navigator::pop,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
actions = {
|
|
||||||
AppBarActions(
|
|
||||||
persistentListOf(
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(SYMR.strings.select_none),
|
|
||||||
icon = Icons.Outlined.Deselect,
|
|
||||||
onClick = { screenModel.massSelect(false) },
|
|
||||||
),
|
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(MR.strings.action_select_all),
|
|
||||||
icon = Icons.Outlined.SelectAll,
|
|
||||||
onClick = { screenModel.massSelect(true) },
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(SYMR.strings.match_enabled_sources),
|
|
||||||
onClick = { screenModel.matchSelection(true) },
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(SYMR.strings.match_pinned_sources),
|
|
||||||
onClick = { screenModel.matchSelection(false) },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = { Text(text = stringResource(MR.strings.action_migrate)) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Outlined.ArrowForward,
|
|
||||||
contentDescription = stringResource(MR.strings.action_migrate),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
screenModel.onMigrationSheet(true)
|
|
||||||
},
|
|
||||||
expanded = fabExpanded,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { contentPadding ->
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
|
||||||
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
|
|
||||||
val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
|
|
||||||
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
|
|
||||||
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
|
|
||||||
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
factory = { context ->
|
|
||||||
screenModel.controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context))
|
|
||||||
screenModel.adapter.value = MigrationSourceAdapter(screenModel.clickListener)
|
|
||||||
screenModel.controllerBinding.root.adapter = screenModel.adapter.value
|
|
||||||
screenModel.adapter.value?.isHandleDragEnabled = true
|
|
||||||
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
|
|
||||||
|
|
||||||
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
|
|
||||||
|
|
||||||
screenModel.controllerBinding.root
|
|
||||||
},
|
|
||||||
update = {
|
|
||||||
screenModel.controllerBinding.root
|
|
||||||
.updatePadding(
|
|
||||||
left = left,
|
|
||||||
top = top,
|
|
||||||
right = right,
|
|
||||||
bottom = bottom,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val migrationSheetOpen by screenModel.migrationSheetOpen.collectAsState()
|
|
||||||
if (migrationSheetOpen) {
|
|
||||||
MigrationBottomSheetDialog(
|
|
||||||
onDismissRequest = { screenModel.onMigrationSheet(false) },
|
|
||||||
onStartMigration = { extraParam ->
|
|
||||||
screenModel.onMigrationSheet(false)
|
|
||||||
screenModel.saveEnabledSources()
|
|
||||||
|
|
||||||
navigator replace MigrationListScreen(MigrationProcedureConfig(migration, extraParam))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
|
|
||||||
navigator.push(
|
|
||||||
if (skipPre) {
|
|
||||||
MigrationListScreen(
|
|
||||||
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PreMigrationScreen(MigrationType.MangaList(mangaIds))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
|
|
||||||
navigator.push(
|
|
||||||
if (skipPre) {
|
|
||||||
MigrationListScreen(
|
|
||||||
MigrationProcedureConfig(MigrationType.MangaSingle(fromMangaId, toManga), null),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PreMigrationScreen(MigrationType.MangaSingle(fromMangaId, toManga))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-137
@@ -1,137 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
|
||||||
|
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class PreMigrationScreenModel(
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
|
||||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
|
||||||
) : ScreenModel {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(emptyList<MigrationSourceItem>())
|
|
||||||
val state = _state.asStateFlow()
|
|
||||||
|
|
||||||
private val _migrationSheetOpen = MutableStateFlow(false)
|
|
||||||
val migrationSheetOpen = _migrationSheetOpen.asStateFlow()
|
|
||||||
|
|
||||||
lateinit var controllerBinding: PreMigrationListBinding
|
|
||||||
var adapter: MutableStateFlow<MigrationSourceAdapter?> = MutableStateFlow(null)
|
|
||||||
|
|
||||||
val clickListener = FlexibleAdapter.OnItemClickListener { _, position ->
|
|
||||||
val adapter = adapter.value ?: return@OnItemClickListener false
|
|
||||||
adapter.getItem(position)?.let {
|
|
||||||
it.sourceEnabled = !it.sourceEnabled
|
|
||||||
}
|
|
||||||
adapter.notifyItemChanged(position)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
val enabledSources = getEnabledSources()
|
|
||||||
_state.update { enabledSources }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of enabled sources ordered by language and name.
|
|
||||||
*
|
|
||||||
* @return list containing enabled sources.
|
|
||||||
*/
|
|
||||||
private fun getEnabledSources(): List<MigrationSourceItem> {
|
|
||||||
val languages = sourcePreferences.enabledLanguages().get()
|
|
||||||
val sourcesSaved = sourcePreferences.migrationSources().get().split("/")
|
|
||||||
.mapNotNull { it.toLongOrNull() }
|
|
||||||
val disabledSources = sourcePreferences.disabledSources().get()
|
|
||||||
.mapNotNull { it.toLongOrNull() }
|
|
||||||
val sources = sourceManager.getVisibleCatalogueSources()
|
|
||||||
.asSequence()
|
|
||||||
.filterIsInstance<HttpSource>()
|
|
||||||
.filter { it.lang in languages }
|
|
||||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
|
||||||
.map {
|
|
||||||
MigrationSourceItem(
|
|
||||||
it,
|
|
||||||
isEnabled(
|
|
||||||
sourcesSaved,
|
|
||||||
disabledSources,
|
|
||||||
it.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
return sources
|
|
||||||
.filter { it.sourceEnabled }
|
|
||||||
.sortedBy { sourcesSaved.indexOf(it.source.id) }
|
|
||||||
.plus(
|
|
||||||
sources.filterNot { it.sourceEnabled },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEnabled(
|
|
||||||
sourcesSaved: List<Long>,
|
|
||||||
disabledSources: List<Long>,
|
|
||||||
id: Long,
|
|
||||||
): Boolean {
|
|
||||||
return if (sourcesSaved.isEmpty()) {
|
|
||||||
id !in disabledSources
|
|
||||||
} else {
|
|
||||||
id in sourcesSaved
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun massSelect(selectAll: Boolean) {
|
|
||||||
val adapter = adapter.value ?: return
|
|
||||||
adapter.currentItems.forEach {
|
|
||||||
it.sourceEnabled = selectAll
|
|
||||||
}
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun matchSelection(matchEnabled: Boolean) {
|
|
||||||
val adapter = adapter.value ?: return
|
|
||||||
val enabledSources = if (matchEnabled) {
|
|
||||||
sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
|
|
||||||
} else {
|
|
||||||
sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
|
|
||||||
}
|
|
||||||
val items = adapter.currentItems.toList()
|
|
||||||
items.forEach {
|
|
||||||
it.sourceEnabled = if (matchEnabled) {
|
|
||||||
it.source.id !in enabledSources
|
|
||||||
} else {
|
|
||||||
it.source.id in enabledSources
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled }
|
|
||||||
adapter.updateDataSet(sortedItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMigrationSheet(isOpen: Boolean) {
|
|
||||||
_migrationSheetOpen.value = isOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveEnabledSources() {
|
|
||||||
val listOfSources = adapter.value?.currentItems
|
|
||||||
?.filterIsInstance<MigrationSourceItem>()
|
|
||||||
?.filter {
|
|
||||||
it.sourceEnabled
|
|
||||||
}
|
|
||||||
?.joinToString("/") { it.source.id.toString() }
|
|
||||||
.orEmpty()
|
|
||||||
|
|
||||||
sourcePreferences.migrationSources().set(listOfSources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-155
@@ -1,155 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
|
||||||
import eu.kanade.presentation.browse.MigrationListScreen
|
|
||||||
import eu.kanade.presentation.browse.components.MigrationExitDialog
|
|
||||||
import eu.kanade.presentation.browse.components.MigrationMangaDialog
|
|
||||||
import eu.kanade.presentation.browse.components.MigrationProgressDialog
|
|
||||||
import eu.kanade.presentation.util.Screen
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import exh.util.overEq
|
|
||||||
import exh.util.underEq
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import tachiyomi.core.common.i18n.pluralStringResource
|
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
|
|
||||||
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {
|
|
||||||
|
|
||||||
var newSelectedItem: Pair<Long, Long>? = null
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
val screenModel = rememberScreenModel { MigrationListScreenModel(config) }
|
|
||||||
val items by screenModel.migratingItems.collectAsState()
|
|
||||||
val migrationDone by screenModel.migrationDone.collectAsState()
|
|
||||||
val unfinishedCount by screenModel.unfinishedCount.collectAsState()
|
|
||||||
val dialog by screenModel.dialog.collectAsState()
|
|
||||||
val migrateProgress by screenModel.migratingProgress.collectAsState()
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
val context = LocalContext.current
|
|
||||||
LaunchedEffect(items) {
|
|
||||||
if (items?.isEmpty() == true) {
|
|
||||||
val manualMigrations = screenModel.manualMigrations.value
|
|
||||||
context.toast(
|
|
||||||
context.pluralStringResource(
|
|
||||||
SYMR.plurals.entry_migrated,
|
|
||||||
manualMigrations,
|
|
||||||
manualMigrations,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (!screenModel.hideNotFound) {
|
|
||||||
navigator.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(newSelectedItem) {
|
|
||||||
if (newSelectedItem != null) {
|
|
||||||
val (oldId, newId) = newSelectedItem!!
|
|
||||||
screenModel.useMangaForMigration(context, newId, oldId)
|
|
||||||
newSelectedItem = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(screenModel) {
|
|
||||||
screenModel.navigateOut.collect {
|
|
||||||
if (items.orEmpty().size == 1 && navigator.items.any { it is MangaScreen }) {
|
|
||||||
val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.id
|
|
||||||
withUIContext {
|
|
||||||
if (mangaId != null) {
|
|
||||||
val newStack = navigator.items.filter {
|
|
||||||
it !is MangaScreen &&
|
|
||||||
it !is MigrationListScreen &&
|
|
||||||
it !is PreMigrationScreen
|
|
||||||
} + MangaScreen(mangaId)
|
|
||||||
navigator replaceAll newStack.first()
|
|
||||||
navigator.push(newStack.drop(1))
|
|
||||||
|
|
||||||
// need to set the navigator in a pop state to dispose of everything properly
|
|
||||||
navigator.push(this@MigrationListScreen)
|
|
||||||
navigator.pop()
|
|
||||||
} else {
|
|
||||||
navigator.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
withUIContext {
|
|
||||||
navigator.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MigrationListScreen(
|
|
||||||
items = items ?: persistentListOf(),
|
|
||||||
migrationDone = migrationDone,
|
|
||||||
unfinishedCount = unfinishedCount,
|
|
||||||
getManga = screenModel::getManga,
|
|
||||||
getChapterInfo = screenModel::getChapterInfo,
|
|
||||||
getSourceName = screenModel::getSourceName,
|
|
||||||
onMigrationItemClick = {
|
|
||||||
navigator.push(MangaScreen(it.id, true))
|
|
||||||
},
|
|
||||||
openMigrationDialog = screenModel::openMigrateDialog,
|
|
||||||
skipManga = { screenModel.removeManga(it) },
|
|
||||||
searchManually = { migrationItem ->
|
|
||||||
val sources = screenModel.getMigrationSources()
|
|
||||||
val validSources = if (sources.size == 1) {
|
|
||||||
sources
|
|
||||||
} else {
|
|
||||||
sources.filter { it.id != migrationItem.manga.source }
|
|
||||||
}
|
|
||||||
val searchScreen = MigrateSearchScreen(migrationItem.manga.id, validSources.map { it.id })
|
|
||||||
navigator push searchScreen
|
|
||||||
},
|
|
||||||
migrateNow = { screenModel.migrateManga(it, false) },
|
|
||||||
copyNow = { screenModel.migrateManga(it, true) },
|
|
||||||
)
|
|
||||||
|
|
||||||
val onDismissRequest = { screenModel.dialog.value = null }
|
|
||||||
when (
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val dialog = dialog
|
|
||||||
) {
|
|
||||||
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
|
||||||
MigrationMangaDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
copy = dialog.copy,
|
|
||||||
mangaSet = dialog.mangaSet,
|
|
||||||
mangaSkipped = dialog.mangaSkipped,
|
|
||||||
copyManga = screenModel::copyMangas,
|
|
||||||
migrateManga = screenModel::migrateMangas,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
|
|
||||||
MigrationExitDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
exitMigration = navigator::pop,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
null -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!migrateProgress.isNaN() && migrateProgress overEq 0f && migrateProgress underEq 1f) {
|
|
||||||
MigrationProgressDialog(
|
|
||||||
progress = migrateProgress,
|
|
||||||
exitMigration = screenModel::cancelMigrate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler(true) {
|
|
||||||
screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-609
@@ -1,609 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
|
||||||
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.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import exh.smartsearch.SmartSourceSearchEngine
|
|
||||||
import exh.source.MERGED_SOURCE_ID
|
|
||||||
import exh.util.ThrottleManager
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
|
||||||
import kotlinx.coroutines.sync.withPermit
|
|
||||||
import logcat.LogPriority
|
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
|
||||||
import tachiyomi.domain.history.model.HistoryUpdate
|
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
|
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import tachiyomi.domain.track.interactor.DeleteTrack
|
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class MigrationListScreenModel(
|
|
||||||
private val config: MigrationProcedureConfig,
|
|
||||||
private val preferences: SourcePreferences = Injekt.get(),
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
|
||||||
private val getManga: GetManga = Injekt.get(),
|
|
||||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
|
||||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
|
||||||
private val getHistoryByMangaId: GetHistory = Injekt.get(),
|
|
||||||
private val upsertHistory: UpsertHistory = Injekt.get(),
|
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
|
||||||
private val deleteTrack: DeleteTrack = Injekt.get(),
|
|
||||||
) : ScreenModel {
|
|
||||||
|
|
||||||
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
|
|
||||||
private val throttleManager = ThrottleManager()
|
|
||||||
|
|
||||||
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
|
|
||||||
val migrationDone = MutableStateFlow(false)
|
|
||||||
val unfinishedCount = MutableStateFlow(0)
|
|
||||||
|
|
||||||
val manualMigrations = MutableStateFlow(0)
|
|
||||||
|
|
||||||
val hideNotFound = preferences.hideNotFoundMigration().get()
|
|
||||||
val showOnlyUpdates = preferences.showOnlyUpdatesMigration().get()
|
|
||||||
|
|
||||||
val navigateOut = MutableSharedFlow<Unit>()
|
|
||||||
|
|
||||||
val dialog = MutableStateFlow<Dialog?>(null)
|
|
||||||
|
|
||||||
val migratingProgress = MutableStateFlow(Float.MAX_VALUE)
|
|
||||||
|
|
||||||
private var migrateJob: Job? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
val mangaIds = when (val migration = config.migration) {
|
|
||||||
is MigrationType.MangaList -> {
|
|
||||||
migration.mangaIds
|
|
||||||
}
|
|
||||||
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
|
|
||||||
}
|
|
||||||
runMigrations(
|
|
||||||
mangaIds
|
|
||||||
.map {
|
|
||||||
async {
|
|
||||||
val manga = getManga.await(it) ?: return@async null
|
|
||||||
MigratingManga(
|
|
||||||
manga = manga,
|
|
||||||
chapterInfo = getChapterInfo(it),
|
|
||||||
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
|
||||||
getMergedReferencesById.await(manga.id)
|
|
||||||
.map { sourceManager.getOrStub(it.mangaSourceId) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
),
|
|
||||||
parentContext = screenModelScope.coroutineContext,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.awaitAll()
|
|
||||||
.filterNotNull()
|
|
||||||
.also {
|
|
||||||
migratingItems.value = it.toImmutableList()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getManga(result: SearchResult.Result) = getManga(result.id)
|
|
||||||
suspend fun getManga(id: Long) = getManga.await(id)
|
|
||||||
suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
|
|
||||||
suspend fun getChapterInfo(id: Long) = getChaptersByMangaId.await(id).let { chapters ->
|
|
||||||
MigratingManga.ChapterInfo(
|
|
||||||
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
|
|
||||||
chapterCount = chapters.size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo(null)
|
|
||||||
|
|
||||||
fun getMigrationSources() = preferences.migrationSources().get().split("/").mapNotNull {
|
|
||||||
val value = it.toLongOrNull() ?: return@mapNotNull null
|
|
||||||
sourceManager.get(value) as? CatalogueSource
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun runMigrations(mangas: List<MigratingManga>) {
|
|
||||||
throttleManager.resetThrottle()
|
|
||||||
unfinishedCount.value = mangas.size
|
|
||||||
val useSourceWithMost = preferences.useSourceWithMost().get()
|
|
||||||
val useSmartSearch = preferences.smartMigration().get()
|
|
||||||
|
|
||||||
val sources = getMigrationSources()
|
|
||||||
for (manga in mangas) {
|
|
||||||
if (!currentCoroutineContext().isActive) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// in case it was removed
|
|
||||||
when (val migration = config.migration) {
|
|
||||||
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
|
||||||
val mangaObj = manga.manga
|
|
||||||
val mangaSource = sourceManager.getOrStub(mangaObj.source)
|
|
||||||
|
|
||||||
val result = try {
|
|
||||||
manga.migrationScope.async {
|
|
||||||
val validSources = if (sources.size == 1) {
|
|
||||||
sources
|
|
||||||
} else {
|
|
||||||
sources.filter { it.id != mangaSource.id }
|
|
||||||
}
|
|
||||||
when (val migration = config.migration) {
|
|
||||||
is MigrationType.MangaSingle -> if (migration.toManga != null) {
|
|
||||||
val localManga = getManga.await(migration.toManga)
|
|
||||||
if (localManga != null) {
|
|
||||||
val source = sourceManager.get(localManga.source) as? CatalogueSource
|
|
||||||
if (source != null) {
|
|
||||||
val chapters = if (source is EHentai) {
|
|
||||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
|
||||||
} else {
|
|
||||||
source.getChapterList(localManga.toSManga())
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
manga.progress.value = validSources.size to validSources.size
|
|
||||||
return@async localManga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
if (useSourceWithMost) {
|
|
||||||
val sourceSemaphore = Semaphore(3)
|
|
||||||
val processedSources = AtomicInteger()
|
|
||||||
|
|
||||||
validSources.map { source ->
|
|
||||||
async async2@{
|
|
||||||
sourceSemaphore.withPermit {
|
|
||||||
try {
|
|
||||||
val searchResult = if (useSmartSearch) {
|
|
||||||
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
|
|
||||||
} else {
|
|
||||||
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchResult != null &&
|
|
||||||
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
|
|
||||||
) {
|
|
||||||
val localManga = networkToLocalManga(searchResult)
|
|
||||||
|
|
||||||
val chapters = if (source is EHentai) {
|
|
||||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
|
||||||
} else {
|
|
||||||
source.getChapterList(localManga.toSManga())
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@async2 null
|
|
||||||
}
|
|
||||||
manga.progress.value =
|
|
||||||
validSources.size to processedSources.incrementAndGet()
|
|
||||||
localManga to chapters.size
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Ignore cancellations
|
|
||||||
throw e
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.mapNotNull { it.await() }.maxByOrNull { it.second }?.first
|
|
||||||
} else {
|
|
||||||
validSources.forEachIndexed { index, source ->
|
|
||||||
val searchResult = try {
|
|
||||||
val searchResult = if (useSmartSearch) {
|
|
||||||
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
|
|
||||||
} else {
|
|
||||||
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchResult != null) {
|
|
||||||
val localManga = networkToLocalManga(searchResult)
|
|
||||||
val chapters = try {
|
|
||||||
if (source is EHentai) {
|
|
||||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
|
||||||
} else {
|
|
||||||
source.getChapterList(localManga.toSManga())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
|
||||||
localManga
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Ignore cancellations
|
|
||||||
throw e
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
manga.progress.value = validSources.size to (index + 1)
|
|
||||||
if (searchResult != null) return@async searchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.await()
|
|
||||||
} catch (_: CancellationException) {
|
|
||||||
// Ignore canceled migrations
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != null && result.thumbnailUrl == null) {
|
|
||||||
try {
|
|
||||||
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga())
|
|
||||||
updateManga.awaitUpdateFromSource(result, newManga, true)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Ignore cancellations
|
|
||||||
throw e
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.searchResult.value = if (result == null) {
|
|
||||||
SearchResult.NotFound
|
|
||||||
} else {
|
|
||||||
SearchResult.Result(result.id)
|
|
||||||
}
|
|
||||||
if (result == null && hideNotFound) {
|
|
||||||
removeManga(manga)
|
|
||||||
}
|
|
||||||
if (result != null &&
|
|
||||||
showOnlyUpdates &&
|
|
||||||
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
|
||||||
) {
|
|
||||||
removeManga(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sourceFinished() {
|
|
||||||
unfinishedCount.value = migratingItems.value.orEmpty().count {
|
|
||||||
it.searchResult.value != SearchResult.Searching
|
|
||||||
}
|
|
||||||
if (allMangasDone()) {
|
|
||||||
migrationDone.value = true
|
|
||||||
}
|
|
||||||
if (migratingItems.value?.isEmpty() == true) {
|
|
||||||
navigateOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun allMangasDone() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } &&
|
|
||||||
migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Result }
|
|
||||||
|
|
||||||
fun mangasSkipped() = migratingItems.value.orEmpty().count { it.searchResult.value == SearchResult.NotFound }
|
|
||||||
|
|
||||||
private suspend fun migrateMangaInternal(
|
|
||||||
prevManga: Manga,
|
|
||||||
manga: Manga,
|
|
||||||
replace: Boolean,
|
|
||||||
) {
|
|
||||||
if (prevManga.id == manga.id) return // Nothing to migrate
|
|
||||||
|
|
||||||
val flags = preferences.migrateFlags().get()
|
|
||||||
// Update chapters read
|
|
||||||
if (MigrationFlags.hasChapters(flags)) {
|
|
||||||
val prevMangaChapters = getChaptersByMangaId.await(prevManga.id)
|
|
||||||
val maxChapterRead = prevMangaChapters.filter(Chapter::read)
|
|
||||||
.maxOfOrNull(Chapter::chapterNumber)
|
|
||||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
|
||||||
val prevHistoryList = getHistoryByMangaId.await(prevManga.id)
|
|
||||||
|
|
||||||
val chapterUpdates = mutableListOf<ChapterUpdate>()
|
|
||||||
val historyUpdates = mutableListOf<HistoryUpdate>()
|
|
||||||
|
|
||||||
dbChapters.forEach { chapter ->
|
|
||||||
if (chapter.isRecognizedNumber) {
|
|
||||||
val prevChapter = prevMangaChapters.find {
|
|
||||||
it.isRecognizedNumber &&
|
|
||||||
it.chapterNumber == chapter.chapterNumber
|
|
||||||
}
|
|
||||||
if (prevChapter != null) {
|
|
||||||
chapterUpdates += ChapterUpdate(
|
|
||||||
id = chapter.id,
|
|
||||||
bookmark = prevChapter.bookmark,
|
|
||||||
read = prevChapter.read,
|
|
||||||
dateFetch = prevChapter.dateFetch,
|
|
||||||
)
|
|
||||||
prevHistoryList.find { it.chapterId == prevChapter.id }?.let { prevHistory ->
|
|
||||||
historyUpdates += HistoryUpdate(
|
|
||||||
chapter.id,
|
|
||||||
prevHistory.readAt ?: return@let,
|
|
||||||
prevHistory.readDuration,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (maxChapterRead != null && chapter.chapterNumber <= maxChapterRead) {
|
|
||||||
chapterUpdates += ChapterUpdate(
|
|
||||||
id = chapter.id,
|
|
||||||
read = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
|
||||||
upsertHistory.awaitAll(historyUpdates)
|
|
||||||
}
|
|
||||||
// Update categories
|
|
||||||
if (MigrationFlags.hasCategories(flags)) {
|
|
||||||
val categories = getCategories.await(prevManga.id)
|
|
||||||
setMangaCategories.await(manga.id, categories.map { it.id })
|
|
||||||
}
|
|
||||||
// Update track
|
|
||||||
if (MigrationFlags.hasTracks(flags)) {
|
|
||||||
val tracks = getTracks.await(prevManga.id)
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
getTracks.await(manga.id).forEach {
|
|
||||||
deleteTrack.await(manga.id, it.trackerId)
|
|
||||||
}
|
|
||||||
insertTrack.awaitAll(tracks.map { it.copy(mangaId = manga.id) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update custom cover
|
|
||||||
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
|
|
||||||
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga.id).inputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
var mangaUpdate = MangaUpdate(manga.id, favorite = true, dateAdded = System.currentTimeMillis())
|
|
||||||
var prevMangaUpdate: MangaUpdate? = null
|
|
||||||
// Update extras
|
|
||||||
if (MigrationFlags.hasExtra(flags)) {
|
|
||||||
mangaUpdate = mangaUpdate.copy(
|
|
||||||
chapterFlags = prevManga.chapterFlags,
|
|
||||||
viewerFlags = prevManga.viewerFlags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Delete downloaded
|
|
||||||
if (MigrationFlags.hasDeleteChapters(flags)) {
|
|
||||||
val oldSource = sourceManager.get(prevManga.source)
|
|
||||||
if (oldSource != null) {
|
|
||||||
downloadManager.deleteManga(prevManga, oldSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update favorite status
|
|
||||||
if (replace) {
|
|
||||||
prevMangaUpdate = MangaUpdate(
|
|
||||||
id = prevManga.id,
|
|
||||||
favorite = false,
|
|
||||||
dateAdded = 0,
|
|
||||||
)
|
|
||||||
mangaUpdate = mangaUpdate.copy(
|
|
||||||
dateAdded = prevManga.dateAdded,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useMangaForMigration(context: Context, newMangaId: Long, selectedMangaId: Long) {
|
|
||||||
val migratingManga = migratingItems.value.orEmpty().find { it.manga.id == selectedMangaId }
|
|
||||||
?: return
|
|
||||||
migratingManga.searchResult.value = SearchResult.Searching
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
val result = migratingManga.migrationScope.async {
|
|
||||||
val manga = getManga(newMangaId)!!
|
|
||||||
val localManga = networkToLocalManga(manga)
|
|
||||||
try {
|
|
||||||
val source = sourceManager.get(manga.source)!!
|
|
||||||
val chapters = source.getChapterList(localManga.toSManga())
|
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@async null
|
|
||||||
}
|
|
||||||
localManga
|
|
||||||
}.await()
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
try {
|
|
||||||
val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga())
|
|
||||||
updateManga.awaitUpdateFromSource(result, newManga, true)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Ignore cancellations
|
|
||||||
throw e
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
|
|
||||||
migratingManga.searchResult.value = SearchResult.Result(result.id)
|
|
||||||
} else {
|
|
||||||
migratingManga.searchResult.value = SearchResult.NotFound
|
|
||||||
withUIContext {
|
|
||||||
context.toast(SYMR.strings.no_chapters_found_for_migration, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun migrateMangas() {
|
|
||||||
migrateMangas(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyMangas() {
|
|
||||||
migrateMangas(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrateMangas(replace: Boolean) {
|
|
||||||
dialog.value = null
|
|
||||||
migrateJob = screenModelScope.launchIO {
|
|
||||||
migratingProgress.value = 0f
|
|
||||||
val items = migratingItems.value.orEmpty()
|
|
||||||
try {
|
|
||||||
items.forEachIndexed { index, manga ->
|
|
||||||
try {
|
|
||||||
ensureActive()
|
|
||||||
val toMangaObj = manga.searchResult.value.let {
|
|
||||||
if (it is SearchResult.Result) {
|
|
||||||
getManga.await(it.id)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (toMangaObj != null) {
|
|
||||||
migrateMangaInternal(
|
|
||||||
manga.manga,
|
|
||||||
toMangaObj,
|
|
||||||
replace,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
logcat(LogPriority.WARN, throwable = e)
|
|
||||||
}
|
|
||||||
migratingProgress.value = index.toFloat() / items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateOut()
|
|
||||||
} finally {
|
|
||||||
migratingProgress.value = Float.MAX_VALUE
|
|
||||||
migrateJob = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelMigrate() {
|
|
||||||
migrateJob?.cancel()
|
|
||||||
migrateJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun navigateOut() {
|
|
||||||
navigateOut.emit(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun migrateManga(mangaId: Long, copy: Boolean) {
|
|
||||||
manualMigrations.value++
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
val manga = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
|
||||||
?: return@launchIO
|
|
||||||
|
|
||||||
val toMangaObj = getManga.await((manga.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO)
|
|
||||||
?: return@launchIO
|
|
||||||
migrateMangaInternal(
|
|
||||||
manga.manga,
|
|
||||||
toMangaObj,
|
|
||||||
!copy,
|
|
||||||
)
|
|
||||||
|
|
||||||
removeManga(mangaId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeManga(mangaId: Long) {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
val item = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
|
||||||
?: return@launchIO
|
|
||||||
removeManga(item)
|
|
||||||
item.migrationScope.cancel()
|
|
||||||
sourceFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeManga(item: MigratingManga) {
|
|
||||||
when (val migration = config.migration) {
|
|
||||||
is MigrationType.MangaList -> {
|
|
||||||
val ids = migration.mangaIds.toMutableList()
|
|
||||||
val index = ids.indexOf(item.manga.id)
|
|
||||||
if (index > -1) {
|
|
||||||
ids.removeAt(index)
|
|
||||||
config.migration = MigrationType.MangaList(ids)
|
|
||||||
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
|
||||||
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MigrationType.MangaSingle -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDispose() {
|
|
||||||
super.onDispose()
|
|
||||||
migratingItems.value.orEmpty().forEach {
|
|
||||||
it.migrationScope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openMigrateDialog(
|
|
||||||
copy: Boolean,
|
|
||||||
) {
|
|
||||||
dialog.value = Dialog.MigrateMangaDialog(
|
|
||||||
copy,
|
|
||||||
migratingItems.value.orEmpty().size,
|
|
||||||
mangasSkipped(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Dialog {
|
|
||||||
data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog()
|
|
||||||
object MigrationExitDialog : Dialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
data class MigrationProcedureConfig(
|
|
||||||
var migration: MigrationType,
|
|
||||||
val extraSearchParams: String?,
|
|
||||||
) : Serializable
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user