diff --git a/.editorconfig b/.editorconfig index 7bee4501e9..1b1fadfbcb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,28 @@ +root = true + [*] charset = utf-8 -end_of_line = lf -indent_style=space -insert_final_newline=true +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true -[*.{json,json5}] -indent_size=2 +[*.xml] +indent_size = 4 +# noinspection EditorConfigKeyCorrectness [*.{kt,kts}] -indent_size=4 -ij_kotlin_allow_trailing_comma=true -ij_kotlin_allow_trailing_comma_on_call_site=true +indent_size = 4 +max_line_length = 120 + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 + +ktlint_code_style = intellij_idea +ktlint_function_naming_ignore_when_annotated_with = Composable +ktlint_standard_class-signature = disabled +ktlint_standard_discouraged-comment-location = disabled +ktlint_standard_function-expression-body = disabled +ktlint_standard_function-signature = disabled diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index bc57d11bf2..545636880b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,9 +35,24 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true - label: I will fill out all of the requested information in this form. required: true + + - type: "textarea" + id: "prioritisation" + attributes: + label: "Is this issue important to you?" + description: | + **Please do not modify this text area!** + + This template let users to vote with a :+1: reaction if they find it important. + This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important. + value: | + Add a :+1: [reaction] to [issues you find important]. + + [reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ + [issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index d313d377e4..a1f0593f0d 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -94,15 +94,30 @@ body: required: true - label: I have written a short but informative title. required: true - - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. + - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help ([Browse → Extensions → Find the extension → Settings → Tap the `[<>]` icon](https://cdn.aap.my.id/extension-repo-link.png), it *should* redirect you to the maintainer). required: true - - label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon) + - label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon). required: true - - label: I have tried the [troubleshooting guide](https://mihon.app/help/). + - label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). required: true - - label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true - label: I have filled out all of the requested information in this form. required: true + + - type: "textarea" + id: "prioritisation" + attributes: + label: "Is this issue important to you?" + description: | + **Please do not modify this text area!** + + This template let users to vote with a :+1: reaction if they find it important. + This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important. + value: | + Add a :+1: [reaction] to [issues you find important]. + + [reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ + [issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..f771cb985a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ + + + +--- + +Add a :+1: [reaction] to [pull requests you find important]. + +[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ +[pull requests you find important]: https://github.com/null2264/yokai/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc diff --git a/.github/workflows/build_check.yml b/.github/workflows/build_check.yml index abb7c971a7..74bb37273e 100644 --- a/.github/workflows/build_check.yml +++ b/.github/workflows/build_check.yml @@ -4,7 +4,15 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -on: [pull_request] +on: + pull_request: + paths: + - '**' + - '!**.md' + - '!i18n/src/commonMain/moko-resources/**/strings.xml' + - '!i18n/src/commonMain/moko-resources/**/plurals.xml' + - 'i18n/src/commonMain/moko-resources/base/strings.xml' + - 'i18n/src/commonMain/moko-resources/base/plurals.xml' jobs: build: @@ -22,18 +30,21 @@ jobs: ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" - name: Setup Gradle - uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29 + uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 with: java: 17 - distro: adopt + distro: temurin - name: Copy CI gradle.properties run: | mkdir -p ~/.gradle cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties - - name: Build and run tests - run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + - name: Build the app + run: ./gradlew assembleStandardRelease + + - name: Run unit tests + run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest - name: Publish test report uses: mikepenz/action-junit-report@v5 diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index c81a5504cf..57f13cf1de 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -43,10 +43,10 @@ jobs: ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" - name: Setup Gradle - uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29 + uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 with: java: 17 - distro: adopt + distro: temurin - name: Setup CHANGELOG parser uses: taiki-e/install-action@parse-changelog @@ -80,6 +80,7 @@ jobs: run: | set -x echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV + echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV # BETA - name: Prepare beta build @@ -88,9 +89,14 @@ jobs: set -x BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g") - [ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1)) + if [ -z "$BETA_COUNT" ]; then + BETA_COUNT="1" + else + BETA_COUNT=$((BETA_COUNT+1)) + fi echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV + echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV # NIGHTLY - name: Prepare nightly build @@ -98,23 +104,17 @@ jobs: run: | set -x echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV + echo "BUILD_TYPE=StandardNightly" >> $GITHUB_ENV echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - # PROD - - name: Build release build and run tests - if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true' - run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + - name: Build the app + if: startsWith(env.BUILD_TYPE, 'Standard') + run: ./gradlew assemble${{ env.BUILD_TYPE }} - # BETA - - name: Build beta build and run tests - if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta == 'true' - run: ./gradlew assembleStandardBeta testReleaseUnitTest testStandardBetaUnitTest - - # NIGHTLY - - name: Build nightly build and run tests - if: startsWith(env.VERSION_TAG, 'r') - run: ./gradlew assembleStandardNightly testReleaseUnitTest testStandardNightlyUnitTest + - name: Run unit tests + if: startsWith(env.BUILD_TYPE, 'Standard') + run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest - name: Upload R8 APK to artifact uses: actions/upload-artifact@v4 @@ -154,7 +154,7 @@ jobs: echo "STAGE=${stage}" >> $GITHUB_OUTPUT - name: Sign APK - uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29 + uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94 if: env.VERSION_TAG != '' with: releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }} diff --git a/.gitignore b/.gitignore index 094f3c64de..fa0048f78b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ */*/build .kotlin/ kls_database.db +weblate.conf diff --git a/.renovaterc.json5 b/.renovaterc.json5 index c28595b97f..6808816504 100644 --- a/.renovaterc.json5 +++ b/.renovaterc.json5 @@ -24,6 +24,8 @@ 'com.github.tachiyomiorg:image-decoder', 'com.github.tachiyomiorg:unifile', 'com.github.tachiyomiorg:conductor-support-preference', + 'com.github.chrisbanes:PhotoView', + 'com.github.PhilJay:MPAndroidChart', ], enabled: false, }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e7a58072..95cea8eab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,101 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - `Additions` - New features - `Changes` - Behaviour/visual changes - `Fixes` - Bugfixes -- `Translation` - Translation changes/updates - `Other` - Technical changes/updates +## [Unreleased] + +### Additions +- Add random library sort +- Add the ability to save search queries +- Add toggle to enable/disable hide source on swipe (@Hiirbaf) +- Add the ability to mark duplicate read chapters as read (@AntsyLich) + +### Changes +- Temporarily disable log file +- Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03) +- Chapter progress now saved everything the page is changed + +### Fixes +- Allow users to bypass onboarding's permission step if Shizuku is installed +- Fix Recents page shows "No recent chapters" instead of a loading screen +- Fix not fully loaded entries can't be selected on Library page +- Fix certain Infinix devices being unable to use any "Open link in browser" actions, including tracker setup (@MajorTanya) +- Fix source filter bottom sheet unable to be fully scrolled to the bottom +- Prevent potential "Comparison method violates its general contract!" crash +- Fix staggered grid cover being squashed for local source (@AwkwardPeak7) + +### Translation +- Update translations from Weblate + +### Other +- Refactor Library to utilize Flow even more +- Refactor EmptyView to use Compose +- Refactor Reader ChapterTransition to use Compose (@arkon) +- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout +- Refactor About page to use Compose +- Adjust Compose-based pages' transition to match J2K's Conductor transition +- Resolve deprecation warnings + - Kotlin's context-receiver, schedule for removal on Kotlin v2.1.x and planned to be replaced by context-parameters on Kotlin v2.2 + - Project.exec -> Providers.exec + - Remove internal API usage to retrieve Kotlin version for kotlin-stdlib +- Move :core module to :core:main + - Move archive related code to :core:archive (@AntsyLich) +- Refactor Library to store LibraryMap instead of flatten list of LibraryItem + - LibraryItem abstraction to make it easier to manage + - LibraryManga no longer extend MangaImpl +- Update dependency gradle to v8.12 +- Update user agent (@Hiirbaf) +- Update serialization to v1.8.1 +- Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1 +- Update lifecycle to v2.9.0 +- Update dependency org.jsoup:jsoup to v1.20.1 +- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0 +- Update dependency io.mockk:mockk to v1.14.2 +- Update dependency io.coil-kt.coil3:coil-bom to v3.2.0 +- Update dependency com.squareup.okio:okio to v3.12.0 +- Update dependency com.google.firebase:firebase-bom to v33.14.0 +- Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0 +- Update dependency com.github.requery:sqlite-android to v3.49.0 +- Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0 +- Update dependency androidx.window:window to v1.4.0 +- Update dependency androidx.webkit:webkit to v1.13.0 +- Update dependency androidx.sqlite:sqlite-ktx to v2.5.1 +- Update dependency androidx.sqlite:sqlite to v2.5.1 +- Update dependency androidx.recyclerview:recyclerview to v1.4.0 +- Update dependency androidx.core:core-ktx to v1.16.0 +- Update dependency androidx.compose:compose-bom to v2025.05.01 +- Update aboutlibraries to v11.6.3 +- Update plugin kotlinter to v5.1.0 +- Update plugin gradle-versions to v0.52.0 +- Update okhttp monorepo to v5.0.0-alpha.16 +- Update moko to v0.24.5 +- Update kotlin monorepo to v2.1.21 +- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2 +- Update dependency me.zhanghai.android.libarchive:library to v1.1.5 +- Update dependency io.insert-koin:koin-bom to v4.0.4 +- Update dependency com.android.tools:desugar_jdk_libs to v2.1.5 +- Update dependency androidx.work:work-runtime-ktx to v2.10.1 +- Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 +- Update plugin firebase-crashlytics to v3.0.3 +- Update null2264/actions digest to 363cb9c +- Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1 + +## [1.9.7.3] + +### Fixes +- More `Comparison method violates its general contract!` crash prevention + +## [1.9.7.2] + +### Fixes +- Fix MyAnimeList timeout issue + +## [1.9.7.1] + +### Fixes +- Prevent `Comparison method violates its general contract!` crashes + ## [1.9.7] ### Changes diff --git a/README.md b/README.md index 33349a0c9b..d4ba494c0d 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,13 @@ A free and open source manga reader -[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg)](https://github.com/null2264/yokai/actions/workflows/build_push.yml) -[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](/LICENSE) [![Discord: Mihon](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon) -[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg)](https://gitlab.com/null2264/yokai) +[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg?labelColor=27303D)](https://gitlab.com/null2264/yokai) +[![Mirror: git.aap](https://img.shields.io/badge/mirror-git.aap-red.svg?labelColor=27303D)](https://git.aap.my.id/null2264/yokai) + +[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg?labelColor=27303D)](https://github.com/null2264/yokai/actions/workflows/build_push.yml) +[![License: Apache-2.0](https://img.shields.io/github/license/null2264/yokai?labelColor=27303D&color=0877d2)](/LICENSE) +[![Translation status](https://img.shields.io/weblate/progress/yokai?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/yokai/) Yokai screenshots diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 00ba2c765b..dc23c131b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,15 +1,13 @@ import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin import com.google.gms.googleservices.GoogleServicesPlugin -import java.io.ByteArrayOutputStream import java.time.LocalDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(androidx.plugins.application) - alias(kotlinx.plugins.android) - alias(kotlinx.plugins.compose.compiler) + id("yokai.android.application") + id("yokai.android.application.compose") alias(kotlinx.plugins.serialization) alias(kotlinx.plugins.parcelize) alias(libs.plugins.aboutlibraries) @@ -23,15 +21,12 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) { } fun runCommand(command: String): String { - val byteOut = ByteArrayOutputStream() - project.exec { - commandLine = command.split(" ") - standardOutput = byteOut - } - return String(byteOut.toByteArray()).trim() + val result = providers.exec { commandLine(command.split(" ")) } + return result.standardOutput.asText.get().trim() } -val _versionName = "1.9.8" +@Suppress("PropertyName") +val _versionName = "1.10.0" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") @@ -54,7 +49,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 157 + versionCode = 158 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true @@ -72,11 +67,6 @@ android { //noinspection ChromeOsAbiSupport abiFilters += supportedAbis } - externalNativeBuild { - cmake { - this.arguments("-DHAVE_LIBJXL=FALSE") - } - } } splits { @@ -122,7 +112,6 @@ android { buildFeatures { viewBinding = true - compose = true // If you're here because there's not BuildConfig, build the app first, it'll generate it for you buildConfig = true @@ -156,7 +145,8 @@ android { } dependencies { - implementation(projects.core) + implementation(projects.core.archive) + implementation(projects.core.main) implementation(projects.data) implementation(projects.domain) implementation(projects.i18n) @@ -251,8 +241,6 @@ dependencies { implementation(libs.shizuku.api) implementation(libs.shizuku.provider) - implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) - implementation(platform(kotlinx.coroutines.bom)) implementation(kotlinx.bundles.coroutines) @@ -280,17 +268,15 @@ tasks { // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { compilerOptions.freeCompilerArgs.addAll( - "-Xcontext-receivers", // "-opt-in=kotlin.Experimental", "-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.ExperimentalStdlibApi", + "-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", - // "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", - "-opt-in=coil3.annotation.ExperimentalCoilApi", // "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", @@ -298,19 +284,6 @@ tasks { "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", ) - - if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") { - compilerOptions.freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - (project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics", - ) - compilerOptions.freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - (project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics", - ) - } } // Duplicating Hebrew string assets due to some locale code issues on different devices diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index b929105a1c..8eb0338127 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -300,7 +300,7 @@ fun buildLogWritersToAdd( ) = buildList { if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter()) - if (logPath != null) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose)) + // if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose)) } private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" diff --git a/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt index 0cd9f75e50..421b83e367 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/appwidget/UpdatesGridGlanceWidget.kt @@ -40,9 +40,13 @@ import eu.kanade.tachiyomi.util.system.launchIO import java.util.Calendar import java.util.Date import kotlin.math.min +import kotlin.math.roundToLong import kotlinx.coroutines.MainScope +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.domain.manga.models.cover +import yokai.domain.recents.interactor.GetRecents class UpdatesGridGlanceWidget : GlanceAppWidget() { private val app: Application by injectLazy() @@ -64,6 +68,33 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() { } } + // FIXME: Don't depends on RecentsPresenter + private suspend fun getUpdates(customAmount: Int = 0, getRecents: GetRecents = Injekt.get()): List> { + return getRecents + .awaitUpdates( + limit = when { + customAmount > 0 -> (customAmount * 1.5).roundToLong() + else -> 25L + } + ) + .mapNotNull { + when { + it.chapter.read || it.chapter.id == null -> RecentsPresenter.getNextChapter(it.manga) + it.history.id == null -> RecentsPresenter.getFirstUpdatedChapter(it.manga, it.chapter) + else -> it.chapter + } ?: return@mapNotNull null + it + } + .asSequence() + .distinctBy { it.manga.id } + .sortedByDescending { it.history.last_read } + // nChapterItems + nAdditionalItems + cReadingItems + .take((RecentsPresenter.UPDATES_CHAPTER_LIMIT * 2) + RecentsPresenter.UPDATES_READING_LIMIT_LOWER) + .filter { it.manga.id != null } + .map { it.manga to it.history.last_read } + .toList() + } + fun loadData(list: List>? = null) { coroutineScope.launchIO { // Don't show anything when lock is active @@ -80,7 +111,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() { .flatMap { manager.getAppWidgetSizes(it) } .maxBy { it.height.value * it.width.value } .calculateRowAndColumnCount() - val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount)) + val processList = list ?: getUpdates(customAmount = min(50, rowCount * columnCount)) data = prepareList(processList, rowCount * columnCount) ids.forEach { update(app, it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/LockedWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/LockedWidget.kt index 7b89f51a37..56a49cd16d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/LockedWidget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/LockedWidget.kt @@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider import eu.kanade.tachiyomi.R -import yokai.i18n.MR import eu.kanade.tachiyomi.appwidget.ContainerModifier import eu.kanade.tachiyomi.appwidget.util.stringResource -import eu.kanade.tachiyomi.ui.main.MainActivity +import yokai.i18n.MR +import yokai.presentation.core.Constants @Composable fun LockedWidget() { val context = LocalContext.current - val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply { + val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } Box( diff --git a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt index d9786f70ad..542b76d3fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/appwidget/components/UpdatesWidget.kt @@ -17,19 +17,18 @@ import androidx.glance.layout.Row import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.padding import androidx.glance.text.Text -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString import eu.kanade.tachiyomi.appwidget.ContainerModifier import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount import eu.kanade.tachiyomi.appwidget.util.stringResource import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity +import yokai.i18n.MR +import yokai.presentation.core.Constants @Composable fun UpdatesWidget(data: List>?) { val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() - val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS) + val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(Constants.SHORTCUT_RECENTS) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) Column( modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)), diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt index be0b8e9773..806b80ea22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt @@ -5,9 +5,17 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import eu.kanade.tachiyomi.core.preference.Preference +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale @Composable fun Preference.collectAsState(): State { val flow = remember(this) { changes() } return flow.collectAsState(initial = get()) } + +fun String.asDateFormat(): DateFormat = when (this) { + "" -> DateFormat.getDateInstance(DateFormat.SHORT) + else -> SimpleDateFormat(this, Locale.getDefault()) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 00521bafe0..fc2f87bacf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -57,8 +57,10 @@ data class BackupManga( @ProtoNumber(805) var customGenre: List? = null, ) { fun getMangaImpl(): MangaImpl { - return MangaImpl().apply { - url = this@BackupManga.url + return MangaImpl( + source = this.source, + url = this.url, + ).apply { title = this@BackupManga.title artist = this@BackupManga.artist author = this@BackupManga.author @@ -67,7 +69,6 @@ data class BackupManga( status = this@BackupManga.status thumbnail_url = this@BackupManga.thumbnailUrl favorite = this@BackupManga.favorite - source = this@BackupManga.source date_added = this@BackupManga.dateAdded viewer_flags = ( this@BackupManga.viewer_flags diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt index a0022e5729..d9969b931b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt @@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models import android.content.Context import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.ui.library.LibrarySort +import java.io.Serializable import yokai.i18n.MR import yokai.util.lang.getString -import java.io.Serializable interface Category : Serializable { @@ -37,8 +37,7 @@ interface Category : Serializable { return ((mangaSort?.minus('a') ?: 0) % 2) != 1 } - fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort) - ?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null + fun sortingMode(): LibrarySort? = LibrarySort.valueOf(mangaSort) val isDragAndDrop get() = ( @@ -56,7 +55,21 @@ interface Category : Serializable { fun mangaOrderToString(): String = if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/") + // For dynamic categories + fun dynamicHeaderKey(): String { + if (!isDynamic) throw IllegalStateException("This category is not a dynamic category") + + return when { + sourceId != null -> "${name}$sourceSplitter${sourceId}" + langId != null -> "${langId}$langSplitter${name}" + else -> name + } + } + companion object { + const val sourceSplitter = "◘•◘" + const val langSplitter = "⨼⨦⨠" + var lastCategoriesAddedTo = emptySet() fun create(name: String): Category = CategoryImpl().apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt index 6e685036ca..de3e30df4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/CategoryImpl.kt @@ -32,10 +32,14 @@ class CategoryImpl : Category { val category = other as Category + if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey() + return name == category.name } override fun hashCode(): Int { + if (isDynamic) return dynamicHeaderKey().hashCode() + return name.hashCode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index 7017d0d14c..e9a27c7c53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.source.model.SChapter fun SChapter.toChapter(): ChapterImpl { return ChapterImpl().apply { - name = this@SChapter.name - url = this@SChapter.url - date_upload = this@SChapter.date_upload - chapter_number = this@SChapter.chapter_number - scanlator = this@SChapter.scanlator + name = this@toChapter.name + url = this@toChapter.url + date_upload = this@toChapter.date_upload + chapter_number = this@toChapter.chapter_number + scanlator = this@toChapter.scanlator } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt index e017568209..7631836fcf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt @@ -1,10 +1,10 @@ package eu.kanade.tachiyomi.data.database.models -import eu.kanade.tachiyomi.ui.library.LibraryItem +import eu.kanade.tachiyomi.domain.manga.models.Manga import kotlin.math.roundToInt -import yokai.data.updateStrategyAdapter data class LibraryManga( + val manga: Manga, var unread: Int = 0, var read: Int = 0, var category: Int = 0, @@ -13,41 +13,11 @@ data class LibraryManga( var latestUpdate: Long = 0, var lastRead: Long = 0, var lastFetch: Long = 0, -) : MangaImpl() { - - var realMangaCount = 0 - get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders") - set(value) { - if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders") - field = value - } - +) { val hasRead get() = read > 0 - @Transient - var items: List? = null - get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders") - set(value) { - if (!isHidden()) throw IllegalStateException("items can only be set by placeholders") - field = value - } - companion object { - fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply { - title = "" - id = Long.MIN_VALUE - category = categoryId - } - - fun createHide(categoryId: Int, title: String, hiddenItems: List): LibraryManga = - createBlank(categoryId).apply { - this.title = title - this.status = -1 - this.read = hiddenItems.size - this.items = hiddenItems - } - fun mapper( // manga id: Long, @@ -78,34 +48,37 @@ data class LibraryManga( latestUpdate: Long, lastRead: Long, lastFetch: Long, - ): LibraryManga = createBlank(categoryId.toInt()).apply { - this.id = id - this.source = source - this.url = url - this.artist = artist - this.author = author - this.description = description - this.genre = genre - this.title = title - this.status = status.toInt() - this.thumbnail_url = thumbnailUrl - this.favorite = favorite - this.last_update = lastUpdate ?: 0L - this.initialized = initialized - this.viewer_flags = viewerFlags.toInt() - this.hide_title = hideTitle - this.chapter_flags = chapterFlags.toInt() - this.date_added = dateAdded ?: 0L - this.filtered_scanlators = filteredScanlators - this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) - this.cover_last_modified = coverLastModified - this.read = readCount.roundToInt() - this.unread = maxOf((total - readCount).roundToInt(), 0) - this.totalChapters = total.toInt() - this.bookmarkCount = bookmarkCount.roundToInt() - this.latestUpdate = latestUpdate - this.lastRead = lastRead - this.lastFetch = lastFetch - } + ): LibraryManga = LibraryManga( + manga = Manga.mapper( + id = id, + source = source, + url = url, + artist = artist, + author = author, + description = description, + genre = genre, + title = title, + status = status, + thumbnailUrl = thumbnailUrl, + favorite = favorite, + lastUpdate = lastUpdate, + initialized = initialized, + viewerFlags = viewerFlags, + hideTitle = hideTitle, + chapterFlags = chapterFlags, + dateAdded = dateAdded, + filteredScanlators = filteredScanlators, + updateStrategy = updateStrategy, + coverLastModified = coverLastModified, + ), + read = readCount.roundToInt(), + unread = maxOf((total - readCount).roundToInt(), 0), + totalChapters = total.toInt(), + bookmarkCount = bookmarkCount.roundToInt(), + category = categoryId.toInt(), + latestUpdate = latestUpdate, + lastRead = lastRead, + lastFetch = lastFetch, + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index fb560da49c..42a0c038eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -182,15 +182,13 @@ var Manga.vibrantCoverColor: Int? id?.let { MangaCoverMetadata.setVibrantColor(it, value) } } -fun Manga.Companion.create(source: Long) = MangaImpl().apply { - this.source = source -} - -fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply { - url = pathUrl - this.title = title - this.source = source -} +fun Manga.Companion.create(url: String, title: String, source: Long = 0) = + MangaImpl( + source = source, + url = url, + ).apply { + this.title = title + } fun Manga.Companion.mapper( id: Long, @@ -213,14 +211,12 @@ fun Manga.Companion.mapper( filteredScanlators: String?, updateStrategy: Long, coverLastModified: Long, -) = create(source).apply { +) = create(url, title, source).apply { this.id = id - this.url = url this.artist = artist this.author = author this.description = description this.genre = genre - this.title = title this.status = status.toInt() this.thumbnail_url = thumbnailUrl this.favorite = favorite diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index afe855bff3..ff0677e9c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -12,7 +12,11 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List = emptyList()) { companion object { - fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl()) + fun createBlank() = MangaChapterHistory( + MangaImpl(null, -1, ""), + ChapterImpl(), + HistoryImpl(), + ) fun mapper( // manga diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index df0342a5d0..6046ddab15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -8,13 +8,11 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy import uy.kohesive.injekt.injectLazy -open class MangaImpl : Manga { - - override var id: Long? = null - - override var source: Long = -1 - - override lateinit var url: String +open class MangaImpl( + override var id: Long? = null, + override var source: Long = -1, + override var url: String = "", +) : Manga { private val customMangaManager: CustomMangaManager by injectLazy() @@ -107,7 +105,7 @@ open class MangaImpl : Manga { } override fun hashCode(): Int { - return if (::url.isInitialized) { + return if (url.isNotBlank()) { url.hashCode() } else { (id ?: 0L).hashCode() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 830c9b071e..8371e1c3b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import yokai.domain.download.DownloadPreferences import yokai.i18n.MR import yokai.util.lang.getString @@ -171,7 +171,7 @@ class DownloadManager( return files.sortedBy { it.name } .mapIndexed { i, file -> - Page(i, uri = file.uri).apply { status = Page.State.READY } + Page(i, uri = file.uri).apply { status = Page.State.Ready } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index fd0960f179..23ac9773b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.download import android.content.Context -import android.os.Looper import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.cache.ChapterCache @@ -55,8 +54,8 @@ import kotlinx.coroutines.supervisorScope import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import yokai.core.archive.ZipWriter import yokai.core.metadata.COMIC_INFO_FILE import yokai.core.metadata.ComicInfo @@ -365,11 +364,11 @@ class Downloader( // Get all the URLs to the source images, fetch pages if necessary pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page -> - page.status = Page.State.LOAD_PAGE + page.status = Page.State.LoadPage try { page.imageUrl = download.source.getImageUrl(page) } catch (e: Throwable) { - page.status = Page.State.ERROR + page.status = Page.State.Error } } @@ -494,12 +493,12 @@ class Downloader( page.uri = file.uri page.progress = 100 - page.status = Page.State.READY + page.status = Page.State.Ready } catch (e: Throwable) { if (e is CancellationException) throw e // Mark this page as error and allow to download the remaining page.progress = 0 - page.status = Page.State.ERROR + page.status = Page.State.Error notifier.onError(e.message, chapName, download.manga.title) } } @@ -518,7 +517,7 @@ class Downloader( tmpDir: UniFile, filename: String, ): UniFile { - page.status = Page.State.DOWNLOAD_IMAGE + page.status = Page.State.DownloadImage page.progress = 0 return flow { val response = source.getImage(page) @@ -604,8 +603,9 @@ class Downloader( dirname: String, tmpDir: UniFile, ) { - val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! - ZipWriter(context, zip).use { writer -> + val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") + if (zip?.isFile != true) throw Exception("Failed to create CBZ file for downloaded chapter") + ZipWriter(context, zip!!).use { writer -> tmpDir.listFiles()?.forEach { file -> writer.write(file) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index de3264b16c..4386bfd43b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -22,7 +22,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { get() = pages?.sumOf(Page::progress) ?: 0 val downloadedImages: Int - get() = pages?.count { it.status == Page.State.READY } ?: 0 + get() = pages?.count { it.status is Page.State.Ready } ?: 0 @Transient private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt index cdcf00262a..8f686664a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt @@ -4,6 +4,7 @@ import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.domain.manga.models.Manga +import java.nio.charset.StandardCharsets import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest @@ -26,7 +27,6 @@ import yokai.domain.library.custom.interactor.GetCustomManga import yokai.domain.library.custom.interactor.RelinkCustomManga import yokai.domain.library.custom.model.CustomMangaInfo import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo -import java.nio.charset.StandardCharsets class CustomMangaManager(val context: Context) { private val scope = CoroutineScope(Dispatchers.IO) @@ -176,8 +176,7 @@ class CustomMangaManager(val context: Context) { val status: Int? = null, ) { - fun toManga() = MangaImpl().apply { - id = this@MangaJson.id + fun toManga() = MangaImpl(id = this.id).apply { title = this@MangaJson.title ?: "" author = this@MangaJson.author artist = this@MangaJson.artist @@ -272,9 +271,6 @@ class CustomMangaManager(val context: Context) { } } - private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply { - this.id = id - this.copyFromComicInfo(comicInfo) - this.title = comicInfo.series?.value ?: "" - } + private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = + MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index b2e8eb44c1..105d61c6a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -173,16 +173,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val mangaList = ( if (savedMangasList != null) { - val mangas = getLibraryManga.await().filter { - it.id in savedMangasList - }.distinctBy { it.id } + val mangas = + getLibraryManga.await() + .filter { it.manga.id in savedMangasList } + .distinctBy { it.manga.id } val categoryId = inputData.getInt(KEY_CATEGORY, -1) if (categoryId > -1) categoryIds.add(categoryId) mangas } else { getMangaToUpdate() } - ).sortedBy { it.title } + ).sortedBy { it.manga.title } return withIOContext { try { @@ -227,7 +228,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun updateChaptersJob(mangaToAdd: List) { // Initialize the variables holding the progress of the updates. mangaToUpdate.addAll(mangaToAdd) - mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source }) + mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source }) checkIfMassiveUpdate() coroutineScope { val list = mangaToUpdateMap.keys.map { source -> @@ -257,42 +258,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private suspend fun updateDetails(mangaToUpdate: List) = coroutineScope { // Initialize the variables holding the progress of the updates. val count = AtomicInteger(0) - val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list -> + val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list -> async { requestSemaphore.withPermit { list.forEach { manga -> ensureActive() - val source = sourceManager.get(manga.source) as? HttpSource ?: return@async + val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async notifier.showProgressNotification( - manga, + manga.manga, count.andIncrement, mangaToUpdate.size, ) ensureActive() val networkManga = try { - source.getMangaDetails(manga.copy()) + source.getMangaDetails(manga.manga.copy()) } catch (e: java.lang.Exception) { Logger.e(e) null } if (networkManga != null) { - manga.prepareCoverUpdate(coverCache, networkManga, false) - val thumbnailUrl = manga.thumbnail_url - manga.copyFrom(networkManga) - manga.initialized = true + manga.manga.prepareCoverUpdate(coverCache, networkManga, false) + val thumbnailUrl = manga.manga.thumbnail_url + manga.manga.copyFrom(networkManga) + manga.manga.initialized = true val request: ImageRequest = - if (thumbnailUrl != manga.thumbnail_url) { + if (thumbnailUrl != manga.manga.thumbnail_url) { // load new covers in background - ImageRequest.Builder(context).data(manga.cover()) + ImageRequest.Builder(context).data(manga.manga.cover()) .memoryCachePolicy(CachePolicy.DISABLED).build() } else { - ImageRequest.Builder(context).data(manga.cover()) + ImageRequest.Builder(context).data(manga.manga.cover()) .memoryCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.WRITE_ONLY) .build() } context.imageLoader.execute(request) - updateManga.await(manga.toMangaUpdate()) + updateManga.await(manga.manga.toMangaUpdate()) } } } @@ -313,9 +314,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val loggedServices = trackManager.services.filter { it.isLogged } mangaToUpdate.forEach { manga -> - notifier.showProgressNotification(manga, count++, mangaToUpdate.size) + notifier.showProgressNotification(manga.manga, count++, mangaToUpdate.size) - val tracks = getTrack.awaitAllByMangaId(manga.id!!) + val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!) tracks.forEach { track -> val service = trackManager.getService(track.sync_id) @@ -324,7 +325,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val newTrack = service.refresh(track) insertTrack.await(newTrack) - syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service) + syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service) } catch (e: Exception) { Logger.e(e) } @@ -376,7 +377,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private fun checkIfMassiveUpdate() { val largestSourceSize = mangaToUpdate - .groupBy { it.source } + .groupBy { it.manga.source } .filterKeys { sourceManager.get(it) !is UnmeteredSource } .maxOfOrNull { it.value.size } ?: 0 if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { @@ -391,7 +392,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val httpSource = sourceManager.get(source) as? HttpSource ?: return false while (count < mangaToUpdateMap[source]!!.size) { val manga = mangaToUpdateMap[source]!![count] - val shouldDownload = manga.shouldDownloadNewChapters(preferences) + val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences) if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) { hasDownloads = true } @@ -410,15 +411,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet try { var hasDownloads = false ensureActive() - notifier.showProgressNotification(manga, progress, mangaToUpdate.size) - val fetchedChapters = source.getChapterList(manga.copy()) + notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size) + val fetchedChapters = source.getChapterList(manga.manga.copy()) if (fetchedChapters.isNotEmpty()) { - val newChapters = syncChaptersWithSource(fetchedChapters, manga, source) + val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source) if (newChapters.first.isNotEmpty()) { if (shouldDownload) { downloadChapters( - manga, + manga.manga, newChapters.first.sortedBy { it.chapter_number }, ) hasDownloads = true @@ -428,24 +429,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } if (deleteRemoved && newChapters.second.isNotEmpty()) { val removedChapters = newChapters.second.filter { - downloadManager.isChapterDownloaded(it, manga) && + downloadManager.isChapterDownloaded(it, manga.manga) && newChapters.first.none { newChapter -> newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank() } } if (removedChapters.isNotEmpty()) { - downloadManager.deleteChapters(removedChapters, manga, source) + downloadManager.deleteChapters(removedChapters, manga.manga, source) } } if (newChapters.first.size + newChapters.second.size > 0) { - sendUpdate(manga.id) + sendUpdate(manga.manga.id) } } return@coroutineScope hasDownloads } catch (e: Exception) { if (e !is CancellationException) { - failedUpdates[manga] = e.message - Logger.e { "Failed updating: ${manga.title}: $e" } + failedUpdates[manga.manga] = e.message + Logger.e { "Failed updating: ${manga.manga.title}: $e" } } return@coroutineScope false } @@ -461,17 +462,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val restrictions = preferences.libraryUpdateMangaRestriction().get() return mangaToAdd.filter { manga -> when { - MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> { - skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed) + MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> { + skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed) } MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> { - skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_caught_up) + skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_caught_up) } MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> { - skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_started) + skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_started) } - manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> { - skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update) + manga.manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> { + skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update) } else -> { return@filter true @@ -503,10 +504,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet preferences.libraryUpdateCategories().get().map(String::toInt) if (categoriesToUpdate.isNotEmpty()) { categoryIds.addAll(categoriesToUpdate) - libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id } + libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id } } else { categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0) - libraryManga.distinctBy { it.id } + libraryManga.distinctBy { it.manga.id } } } @@ -564,13 +565,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } private fun addMangaToQueue(categoryId: Int, manga: List) { - val mangas = filterMangaToUpdate(manga).sortedBy { it.title } + val mangas = filterMangaToUpdate(manga).sortedBy { it.manga.title } categoryIds.add(categoryId) addManga(mangas) } private fun addCategory(categoryId: Int) { - val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title } + val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.manga.title } categoryIds.add(categoryId) addManga(mangas) } @@ -579,7 +580,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val distinctManga = mangaToAdd.filter { it !in mangaToUpdate } mangaToUpdate.addAll(distinctManga) checkIfMassiveUpdate() - distinctManga.groupBy { it.source }.forEach { + distinctManga.groupBy { it.manga.source }.forEach { // if added queue items is a new source not in the async list or an async list has // finished running if (mangaToUpdateMap[it.key].isNullOrEmpty()) { @@ -727,9 +728,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (mangaToUse != null) { builder.putLongArray( KEY_MANGAS, - mangaToUse.firstOrNull()?.id?.let { longArrayOf(it) } ?: longArrayOf(), + mangaToUse.firstOrNull()?.manga?.id?.let { longArrayOf(it) } ?: longArrayOf(), ) - extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.id } + extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.manga.id } } } val inputData = builder.build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index fd878892e7..323361883f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) { val manga = it.key val chapters = it.value val chapterNames = chapters.map { chapter -> - chapter.preferredChapterName(context, manga, preferences) + chapter.preferredChapterName(context, manga.manga, preferences) } notifications.add( Pair( context.notification(Notifications.CHANNEL_NEW_CHAPTERS) { setSmallIcon(R.drawable.ic_yokai) try { - val request = ImageRequest.Builder(context).data(manga.cover()) + val request = ImageRequest.Builder(context).data(manga.manga.cover()) .networkCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.ENABLED) .transformations(CircleCropTransformation()) @@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) { } catch (_: Exception) { } setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - setContentTitle(manga.title) + setContentTitle(manga.manga.title) color = ContextCompat.getColor(context, R.color.secondaryTachiyomi) val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) { "${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " + @@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) { setContentIntent( NotificationReceiver.openChapterPendingActivity( context, - manga, + manga.manga, chapters.first(), ), ) @@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) { context.getString(MR.strings.mark_as_read), NotificationReceiver.markAsReadPendingBroadcast( context, - manga, + manga.manga, chapters, Notifications.ID_NEW_CHAPTERS, ), @@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) { context.getString(MR.strings.view_chapters), NotificationReceiver.openChapterPendingActivity( context, - manga, + manga.manga, Notifications.ID_NEW_CHAPTERS, ), ) setAutoCancel(true) }, - manga.id.hashCode(), + manga.manga.id.hashCode(), ), ) } @@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) { NotificationCompat.BigTextStyle() .bigText( updates.keys.joinToString("\n") { - it.title.chop(45) + it.manga.title.chop(45) }, ), ) } } else if (!preferences.hideNotificationContent().get()) { - setContentText(updates.keys.first().title.chop(45)) + setContentText(updates.keys.first().manga.title.chop(45)) } priority = NotificationCompat.PRIORITY_HIGH setGroup(Notifications.GROUP_NEW_CHAPTERS) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 649ead87a9..57af3da28a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -487,7 +487,7 @@ class NotificationReceiver : BroadcastReceiver() { */ internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent { val newIntent = - Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA) + Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) .putExtra(Constants.MANGA_EXTRA, manga.id) .putExtra("notificationId", manga.id.hashCode()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index ec29bc9262..5d63f59b83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.getEnum +import eu.kanade.tachiyomi.core.storage.preference.asDateFormat import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder @@ -19,13 +20,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.util.system.Themes +import java.text.DateFormat +import java.util.Locale import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values @@ -187,10 +187,10 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10") - fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) { - "" -> DateFormat.getDateInstance(DateFormat.SHORT) - else -> SimpleDateFormat(format, Locale.getDefault()) - } + fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "") + + @Deprecated("Use dateFormatRaw().get().asDateFormat() instead") + fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat() fun appLanguage() = preferenceStore.getString("app_language", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index d86b58ac72..69e554f1aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -49,15 +49,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("completedAt", createDate(track.finished_reading_date)) } } - with(json) { - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .let { - track.library_id = it.data.entry.id - track - } - } + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .let { + track.library_id = it.data.entry.id + track + } } } @@ -74,11 +72,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("completedAt", createDate(track.finished_reading_date)) } } - with(json) { - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - track - } + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + track } } @@ -90,13 +86,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("query", search) } } - with(json) { - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .data.page.media - .map { it.toALManga().toTrack() } - } + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .data.page.media + .map { it.toALManga().toTrack() } } } @@ -109,15 +103,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("manga_id", track.media_id) } } - with(json) { - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - .parseAs() - .data.page.mediaList - .map { it.toALUserManga() } - .firstOrNull() - ?.toTrack() - } + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .data.page.mediaList + .map { it.toALUserManga() } + .firstOrNull() + ?.toTrack() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 77c6694a20..44fc5c9d5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -75,29 +75,25 @@ class BangumiApi( .appendQueryParameter("responseGroup", "large") .appendQueryParameter("max_results", "20") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { result -> - if (result.code == 404) emptyList() + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { result -> + if (result.code == 404) emptyList() - result.list - ?.map { it.toTrackSearch(trackId) } - .orEmpty() - } - } + result.list + ?.map { it.toTrackSearch(trackId) } + .orEmpty() + } } } suspend fun findLibManga(track: Track): Track? { return withIOContext { - with(json) { - authClient.newCall(GET("$API_URL/subject/${track.media_id}")) - .awaitSuccess() - .parseAs() - .toTrackSearch(trackId) - } + authClient.newCall(GET("$API_URL/subject/${track.media_id}")) + .awaitSuccess() + .parseAs() + .toTrackSearch(trackId) } } @@ -111,29 +107,25 @@ class BangumiApi( .build() // TODO: get user readed chapter here - with(json) { - authClient.newCall(requestUserRead) - .awaitSuccess() - .parseAs() - .let { - if (it.code == 400) return@let null + authClient.newCall(requestUserRead) + .awaitSuccess() + .parseAs() + .let { + if (it.code == 400) return@let null - track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS - track.last_chapter_read = it.epStatus!!.toFloat() - track.score = it.rating!!.toFloat() - track - } - } + track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS + track.last_chapter_read = it.epStatus!!.toFloat() + track.score = it.rating!!.toFloat() + track + } } } suspend fun accessToken(code: String): BGMOAuth { return withIOContext { - with(json) { - client.newCall(accessTokenRequest(code)) - .awaitSuccess() - .parseAs() - } + client.newCall(accessTokenRequest(code)) + .awaitSuccess() + .parseAs() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt index 2157c1b69d..f737edc924 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt @@ -44,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor try { client.newCall(request).execute().use { when (it.code) { - 200 -> return with(json) { it.parseAs().token } + 200 -> return it.parseAs().token 401 -> { Logger.w { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" } throw IOException("Unauthorized / api key not valid") @@ -89,11 +89,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor private fun getTotalChapters(url: String): Long { val requestUrl = getApiVolumesUrl(url) try { - val listVolumeDto = with(json) { + val listVolumeDto = authClient.newCall(GET(requestUrl)) .execute() .parseAs>() - } var volumeNumber = 0L var maxChapterNumber = 0L for (volume in listVolumeDto) { @@ -117,9 +116,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor try { authClient.newCall(GET(requestUrl)).execute().use { if (it.code == 200) { - return with(json) { - it.parseAs().number!!.replace(",", ".").toFloat() - } + return it.parseAs().number!!.replace(",", ".").toFloat() } if (it.code == 204) { return 0F @@ -134,11 +131,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { try { - val serieDto: SeriesDto = with(json) { + val serieDto: SeriesDto = authClient.newCall(GET(url)) .awaitSuccess() .parseAs() - } val track = serieDto.toTrack() track.apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 288d9e5c18..b01a0a3a7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -131,14 +131,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun search(query: String): List { return withIOContext { - with(json) { - authClient.newCall(GET(ALGOLIA_KEY_URL)) - .awaitSuccess() - .parseAs() - .let { - algoliaSearch(it.media.key, query) - } - } + authClient.newCall(GET(ALGOLIA_KEY_URL)) + .awaitSuccess() + .parseAs() + .let { + algoliaSearch(it.media.key, query) + } } } @@ -147,25 +145,23 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val jsonObject = buildJsonObject { put("params", "query=$query$ALGOLIA_FILTER") } - with(json) { - client.newCall( - POST( - ALGOLIA_URL, - headers = headersOf( - "X-Algolia-Application-Id", - ALGOLIA_APP_ID, - "X-Algolia-API-Key", - key, - ), - body = jsonObject.toString().toRequestBody(jsonMime), + client.newCall( + POST( + ALGOLIA_URL, + headers = headersOf( + "X-Algolia-Application-Id", + ALGOLIA_APP_ID, + "X-Algolia-API-Key", + key, ), - ) - .awaitSuccess() - .parseAs() - .hits - .filter { it.subtype != "novel" } - .map { it.toTrack() } - } + body = jsonObject.toString().toRequestBody(jsonMime), + ), + ) + .awaitSuccess() + .parseAs() + .hits + .filter { it.subtype != "novel" } + .map { it.toTrack() } } } @@ -175,18 +171,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") .appendQueryParameter("include", "manga") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - if (it.data.isNotEmpty() && it.included.isNotEmpty()) { - it.firstToTrack() - } else { - null - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToTrack() + } else { + null } - } + } } } @@ -196,18 +190,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .encodedQuery("filter[id]=${track.media_id}") .appendQueryParameter("include", "manga") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - if (it.data.isNotEmpty() && it.included.isNotEmpty()) { - it.firstToTrack() - } else { - throw Exception("Could not find manga") - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToTrack() + } else { + throw Exception("Could not find manga") } - } + } } } @@ -220,11 +212,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) .add("client_id", CLIENT_ID) .add("client_secret", CLIENT_SECRET) .build() - with(json) { - client.newCall(POST(LOGIN_URL, body = formBody)) - .awaitSuccess() - .parseAs() - } + client.newCall(POST(LOGIN_URL, body = formBody)) + .awaitSuccess() + .parseAs() } } @@ -233,13 +223,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val url = "${BASE_URL}users".toUri().buildUpon() .encodedQuery("filter[self]=true") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .data[0] - .id - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .data[0] + .id } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt index 68f3bed857..ab2ffd327c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt @@ -26,21 +26,19 @@ class KomgaApi(private val client: OkHttpClient) { withIOContext { try { val track = - with(json) { - if (url.contains(READLIST_API)) { - client.newCall(GET(url)) - .awaitSuccess() - .parseAs() - .toTrack() - } else { - client.newCall(GET(url)) - .awaitSuccess() - .parseAs() - .toTrack() - } + if (url.contains(READLIST_API)) { + client.newCall(GET(url)) + .awaitSuccess() + .parseAs() + .toTrack() + } else { + client.newCall(GET(url)) + .awaitSuccess() + .parseAs() + .toTrack() } - val progress = with(json) { + val progress = client .newCall( GET( @@ -59,7 +57,6 @@ class KomgaApi(private val client: OkHttpClient) { it.parseAs().toV2() } } - } track.apply { cover_url = "$url/thumbnail" tracking_url = url diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index ab664dd504..a4b8887c83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -37,15 +37,13 @@ class MangaUpdatesApi( suspend fun getSeriesListItem(track: Track): Pair { val listItem = - with(json) { - authClient.newCall( - GET( - url = "$BASE_URL/v1/lists/series/${track.media_id}", - ), - ) - .awaitSuccess() - .parseAs() - } + authClient.newCall( + GET( + url = "$BASE_URL/v1/lists/series/${track.media_id}", + ), + ) + .awaitSuccess() + .parseAs() val rating = getSeriesRating(track) @@ -104,15 +102,13 @@ class MangaUpdatesApi( private suspend fun getSeriesRating(track: Track): MURating? { return try { - with(json) { - authClient.newCall( - GET( - url = "$BASE_URL/v1/series/${track.media_id}/rating", - ), - ) - .awaitSuccess() - .parseAs() - } + authClient.newCall( + GET( + url = "$BASE_URL/v1/series/${track.media_id}/rating", + ), + ) + .awaitSuccess() + .parseAs() } catch (e: Exception) { null } @@ -151,18 +147,16 @@ class MangaUpdatesApi( }, ) } - return with(json) { - client.newCall( - POST( - url = "$BASE_URL/v1/series/search", - body = body.toString().toRequestBody(CONTENT_TYPE), - ), - ) - .awaitSuccess() - .parseAs() - .results - .map { it.record } - } + return client.newCall( + POST( + url = "$BASE_URL/v1/series/search", + body = body.toString().toRequestBody(CONTENT_TYPE), + ), + ) + .awaitSuccess() + .parseAs() + .results + .map { it.record } } suspend fun authenticate(username: String, password: String): MUContext? { @@ -170,17 +164,15 @@ class MangaUpdatesApi( put("username", username) put("password", password) } - return with(json) { - client.newCall( - PUT( - url = "$BASE_URL/v1/account/login", - body = body.toString().toRequestBody(CONTENT_TYPE), - ), - ) - .awaitSuccess() - .parseAs() - .context - } + return client.newCall( + PUT( + url = "$BASE_URL/v1/account/login", + body = body.toString().toRequestBody(CONTENT_TYPE), + ), + ) + .awaitSuccess() + .parseAs() + .context } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 4a5bc58d0a..b3411cd2d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -45,11 +45,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .add("code_verifier", codeVerifier) .add("grant_type", "authorization_code") .build() - with(json) { - client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) - .awaitSuccess() - .parseAs() - } + client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) + .awaitSuccess() + .parseAs() } } @@ -59,12 +57,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url("$BASE_API_URL/users/@me") .get() .build() - with(json) { - authClient.newCall(request) - .awaitSuccess() - .parseAs() - .name - } + authClient.newCall(request) + .awaitSuccess() + .parseAs() + .name } } @@ -75,15 +71,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendQueryParameter("q", query.take(64)) .appendQueryParameter("nsfw", "true") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .data - .map { async { getMangaDetails(it.node.id) } } - .awaitAll() - .filter { !it.publishing_type.contains("novel") } - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .data + .map { async { getMangaDetails(it.node.id) } } + .awaitAll() + .filter { !it.publishing_type.contains("novel") } } } @@ -93,24 +87,22 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath(id.toString()) .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .let { - TrackSearch.create(TrackManager.MYANIMELIST).apply { - media_id = it.id - title = it.title - summary = it.synopsis - total_chapters = it.numChapters - cover_url = it.covers.large - tracking_url = "https://myanimelist.net/manga/$media_id" - publishing_status = it.status.replace("_", " ") - publishing_type = it.mediaType.replace("_", " ") - start_date = it.startDate ?: "" - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { + TrackSearch.create(TrackManager.MYANIMELIST).apply { + media_id = it.id + title = it.title + summary = it.synopsis + total_chapters = it.numChapters + cover_url = (it.covers?.large ?: it.covers?.medium).orEmpty() + tracking_url = "https://myanimelist.net/manga/$media_id" + publishing_status = it.status.replace("_", " ") + publishing_type = it.mediaType.replace("_", " ") + start_date = it.startDate ?: "" } - } + } } } @@ -132,12 +124,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url(mangaUrl(track.media_id).toString()) .put(formBodyBuilder.build()) .build() - with(json) { - authClient.newCall(request) - .awaitSuccess() - .parseAs() - .let { parseMangaItem(it, track) } - } + authClient.newCall(request) + .awaitSuccess() + .parseAs() + .let { parseMangaItem(it, track) } } } @@ -147,15 +137,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath(track.media_id.toString()) .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .build() - with(json) { - authClient.newCall(GET(uri.toString())) - .awaitSuccess() - .parseAs() - .let { item -> - track.total_chapters = item.numChapters - item.myListStatus?.let { parseMangaItem(it, track) } - } - } + authClient.newCall(GET(uri.toString())) + .awaitSuccess() + .parseAs() + .let { item -> + track.total_chapters = item.numChapters + item.myListStatus?.let { parseMangaItem(it, track) } + } } } @@ -190,11 +178,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .url(urlBuilder.build().toString()) .get() .build() - with(json) { - authClient.newCall(request) - .awaitSuccess() - .parseAs() - } + authClient.newCall(request) + .awaitSuccess() + .parseAs() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 9b7de62837..412e4a49ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -34,7 +34,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor // Add the authorization header to the original request val authRequest = originalRequest.newBuilder() .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") - .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") + // .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() return chain.proceed(authRequest) @@ -66,7 +66,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor return runCatching { if (response.isSuccessful) { - with(json) { response.parseAs() } + response.parseAs() } else { response.close() null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt index c4ab92ee92..6950c54960 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt @@ -12,7 +12,7 @@ data class MALManga( val numChapters: Long, val mean: Double = -1.0, @SerialName("main_picture") - val covers: MALMangaCovers, + val covers: MALMangaCovers?, val status: String, @SerialName("media_type") val mediaType: String, @@ -22,5 +22,6 @@ data class MALManga( @Serializable data class MALMangaCovers( - val large: String = "", + val large: String?, + val medium: String, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 456acbdfdd..61286976fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -74,12 +74,10 @@ class ShikimoriApi( .appendQueryParameter("search", search) .appendQueryParameter("limit", "20") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs>() - .map { it.toTrack(trackId) } - } + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs>() + .map { it.toTrack(trackId) } } } @@ -102,51 +100,44 @@ class ShikimoriApi( val urlMangas = "$API_URL/mangas".toUri().buildUpon() .appendPath(track.media_id.toString()) .build() - val manga = with(json) { + val manga = authClient.newCall(GET(urlMangas.toString())) .awaitSuccess() .parseAs() - } val url = "$API_URL/v2/user_rates".toUri().buildUpon() .appendQueryParameter("user_id", user_id) .appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_type", "Manga") .build() - with(json) { - authClient.newCall(GET(url.toString())) - .execute() - .parseAs>() - .let { entries -> - if (entries.size > 1) { - throw Exception("Too manga manga in response") - } - entries - .map { it.toTrack(trackId, manga) } - .firstOrNull() + authClient.newCall(GET(url.toString())) + .execute() + .parseAs>() + .let { entries -> + if (entries.size > 1) { + throw Exception("Too manga manga in response") } - } + entries + .map { it.toTrack(trackId, manga) } + .firstOrNull() + } } } suspend fun getCurrentUser(): Int { return withIOContext { - with(json) { - authClient.newCall(GET("$API_URL/users/whoami")) - .awaitSuccess() - .parseAs() - .id - } + authClient.newCall(GET("$API_URL/users/whoami")) + .awaitSuccess() + .parseAs() + .id } } suspend fun accessToken(code: String): SMOAuth { return withIOContext { - with(json) { - client.newCall(accessTokenRequest(code)) - .awaitSuccess() - .parseAs() - } + client.newCall(accessTokenRequest(code)) + .awaitSuccess() + .parseAs() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt index 7c8f70f519..7d67193034 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt @@ -52,9 +52,7 @@ class TachideskApi { trackUrl } - val manga = with(json) { - client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs() - } + val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs() TrackSearch.create(TrackManager.SUWAYOMI).apply { title = manga.title @@ -74,9 +72,7 @@ class TachideskApi { suspend fun updateProgress(track: Track): Track { val url = track.tracking_url - val chapters = with(json) { - client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs>() - } + val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs>() val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index client.newCall( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index 1ee4855567..05d7755975 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.withIOContext +import java.util.Date +import java.util.concurrent.TimeUnit import kotlinx.serialization.json.Json import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.domain.base.models.Version -import java.util.* -import java.util.concurrent.* class AppUpdateChecker( private val json: Json = Injekt.get(), @@ -31,48 +31,46 @@ class AppUpdateChecker( } return withIOContext { - val result = with(json) { - if (preferences.checkForBetas().get()) { - networkService.client - .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases")) - .await() - .parseAs>() - .let { githubReleases -> - val releases = - githubReleases.take(10).filter { isNewVersion(it.version) } - // Check if any of the latest versions are newer than the current version - val release = releases - .maxWithOrNull { r1, r2 -> - when { - r1.version == r2.version -> 0 - isNewVersion(r2.version, r1.version) -> -1 - else -> 1 - } + val result = if (preferences.checkForBetas().get()) { + networkService.client + .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases")) + .await() + .parseAs>() + .let { githubReleases -> + val releases = + githubReleases.take(10).filter { isNewVersion(it.version) } + // Check if any of the latest versions are newer than the current version + val release = releases + .maxWithOrNull { r1, r2 -> + when { + r1.version == r2.version -> 0 + isNewVersion(r2.version, r1.version) -> -1 + else -> 1 } - preferences.lastAppCheck().set(Date().time) - - if (release != null) { - AppUpdateResult.NewUpdate(release) - } else { - AppUpdateResult.NoNewUpdate } - } - } else { - networkService.client - .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) - .await() - .parseAs() - .let { - preferences.lastAppCheck().set(Date().time) + preferences.lastAppCheck().set(Date().time) - // Check if latest version is newer than the current version - if (isNewVersion(it.version)) { - AppUpdateResult.NewUpdate(it) - } else { - AppUpdateResult.NoNewUpdate - } + if (release != null) { + AppUpdateResult.NewUpdate(release) + } else { + AppUpdateResult.NoNewUpdate } - } + } + } else { + networkService.client + .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) + .await() + .parseAs() + .let { + preferences.lastAppCheck().set(Date().time) + + // Check if latest version is newer than the current version + if (isNewVersion(it.version)) { + AppUpdateResult.NewUpdate(it) + } else { + AppUpdateResult.NoNewUpdate + } + } } if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 3492704094..d128020810 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -7,6 +7,7 @@ import android.os.Parcelable import co.touchlab.kermit.Logger import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.extension.api.ExtensionApi +import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.LoadResult @@ -17,6 +18,9 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.withIOContext +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow @@ -27,8 +31,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.domain.base.BasePreferences import yokai.domain.extension.interactor.TrustExtension -import java.util.* -import java.util.concurrent.* /** * The manager of extensions installed as another apk which extend the available sources. It handles diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index a33e307fb6..0eaf7c2f80 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.util.system.withIOContext import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -24,7 +23,6 @@ import yokai.domain.extension.repo.model.ExtensionRepo internal class ExtensionApi { - private val json: Json by injectLazy() private val networkService: NetworkHelper by injectLazy() private val getExtensionRepo: GetExtensionRepo by injectLazy() private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() @@ -47,11 +45,9 @@ internal class ExtensionApi { .newCall(GET("$repoBaseUrl/index.min.json")) .awaitSuccess() - with(json) { - response - .parseAs>() - .toExtensions(repoBaseUrl) - } + response + .parseAs>() + .toExtensions(repoBaseUrl) } catch (e: Throwable) { Logger.e(e) { "Failed to get extensions from $repoBaseUrl" } emptyList() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt new file mode 100644 index 0000000000..f759a572ee --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -0,0 +1,122 @@ +package eu.kanade.tachiyomi.extension.installer + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.annotation.CallSuper +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import eu.kanade.tachiyomi.extension.ExtensionManager +import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID +import java.util.Collections +import java.util.concurrent.atomic.AtomicReference +import uy.kohesive.injekt.injectLazy + +abstract class Installer( + internal val context: Context, + // TODO: Remove finishedQueue + internal val finishedQueue: (Installer) -> Unit, +) { + + private val extensionManager: ExtensionManager by injectLazy() + + private var waitingInstall = AtomicReference(null) + private val queue = Collections.synchronizedList(mutableListOf()) + + private val cancelReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return + cancelQueue(downloadId) + } + } + + abstract var ready: Boolean + + fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName } + + /** + * Add an item to install queue. + * + * @param downloadId Download ID as known by [ExtensionManager] + * @param uri Uri of APK to install + */ + fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) { + queue.add(Entry(downloadId, pkgName, uri)) + checkQueue() + } + + @CallSuper + open fun processEntry(entry: Entry) { + extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode()) + } + + open fun cancelEntry(entry: Entry): Boolean { + return true + } + + /** + * Tells the queue to continue processing the next entry and updates the install step + * of the completed entry ([waitingInstall]) to [ExtensionManager]. + * + * @param resultStep new install step for the processed entry. + * @see waitingInstall + */ + fun continueQueue(succeeded: Boolean) { + val completedEntry = waitingInstall.getAndSet(null) + if (completedEntry != null) { + extensionManager.setInstallationResult(completedEntry.downloadId, succeeded) + checkQueue() + } + } + + fun checkQueue() { + if (!ready) { + return + } + if (queue.isEmpty()) { + finishedQueue(this) + return + } + val nextEntry = queue.first() + if (waitingInstall.compareAndSet(null, nextEntry)) { + queue.removeAt(0) + processEntry(nextEntry) + } + } + + @CallSuper + open fun onDestroy() { + LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver) + queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) } + queue.clear() + waitingInstall.set(null) + } + + protected fun getActiveEntry(): Entry? = waitingInstall.get() + + /** + * Cancels queue for the provided download ID if exists. + * + * @param downloadId Download ID as known by [ExtensionManager] + */ + fun cancelQueue(downloadId: Long) { + val waitingInstall = this.waitingInstall.get() + val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return + if (cancelEntry(toCancel)) { + queue.remove(toCancel) + if (waitingInstall == toCancel) { + // Currently processing removed entry, continue queue + this.waitingInstall.set(null) + checkQueue() + } + queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) } +// extensionManager.up(downloadId, InstallStep.Idle) + } + } + + data class Entry( + val downloadId: Long, + val pkgName: String, + val uri: Uri, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt similarity index 55% rename from app/src/main/java/eu/kanade/tachiyomi/extension/ShizukuInstaller.kt rename to app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index de12d213bb..ae0307b6a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -1,21 +1,16 @@ -package eu.kanade.tachiyomi.extension +package eu.kanade.tachiyomi.extension.installer -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import android.os.Process -import androidx.localbroadcastmanager.content.LocalBroadcastManager import co.touchlab.kermit.Logger -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource -import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID import eu.kanade.tachiyomi.util.system.getUriSize -import eu.kanade.tachiyomi.util.system.isPackageInstalled +import eu.kanade.tachiyomi.util.system.isShizukuInstalled +import java.io.BufferedReader +import java.io.InputStream +import java.lang.reflect.Method +import java.util.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -23,38 +18,21 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuRemoteProcess -import rikka.sui.Sui -import uy.kohesive.injekt.injectLazy -import java.io.BufferedReader -import java.io.InputStream -import java.lang.reflect.Method -import java.util.* -import java.util.concurrent.atomic.AtomicReference +import yokai.i18n.MR +import yokai.util.lang.getString -class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) { +class ShizukuInstaller( + context: Context, + finishedQueue: (Installer) -> Unit, +) : Installer(context, finishedQueue) { - private val extensionManager: ExtensionManager by injectLazy() - - private var waitingInstall = AtomicReference(null) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val cancelReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return - cancelQueue(downloadId) - } - } - - data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri) - private val queue = Collections.synchronizedList(mutableListOf()) - private val shizukuDeadListener = Shizuku.OnBinderDeadListener { Logger.d { "Shizuku was killed prematurely" } finishedQueue(this) } - fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName } - private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener { override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { @@ -69,13 +47,13 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku } } - var ready = false + override var ready = false private val newProcess: Method init { Shizuku.addBinderDeadListener(shizukuDeadListener) - require(Shizuku.pingBinder() && (context.isPackageInstalled(shizukuPkgName) || Sui.isSui())) { + require(Shizuku.pingBinder() && context.isShizukuInstalled) { finishedQueue(this) context.getString(MR.strings.ext_installer_shizuku_stopped) } @@ -91,9 +69,8 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku newProcess.isAccessible = true } - @Suppress("BlockingMethodInNonBlockingContext") - fun processEntry(entry: Entry) { - extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode()) + override fun processEntry(entry: Entry) { + super.processEntry(entry) ioScope.launch { var sessionId: String? = null try { @@ -131,85 +108,14 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku } } - /** - * Checks the queue. The provided service will be stopped if the queue is empty. - * Will not be run when not ready. - * - * @see ready - */ - fun checkQueue() { - if (!ready) { - return - } - if (queue.isEmpty()) { - finishedQueue(this) - return - } - val nextEntry = queue.first() - if (waitingInstall.compareAndSet(null, nextEntry)) { - queue.removeAt(0) - processEntry(nextEntry) - } - } - - /** - * Tells the queue to continue processing the next entry and updates the install step - * of the completed entry ([waitingInstall]) to [ExtensionManager]. - * - * @param resultStep new install step for the processed entry. - * @see waitingInstall - */ - fun continueQueue(succeeded: Boolean) { - val completedEntry = waitingInstall.getAndSet(null) - if (completedEntry != null) { - extensionManager.setInstallationResult(completedEntry.downloadId, succeeded) - checkQueue() - } - } - - /** - * Add an item to install queue. - * - * @param downloadId Download ID as known by [ExtensionManager] - * @param uri Uri of APK to install - */ - fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) { - queue.add(Entry(downloadId, pkgName, uri)) - checkQueue() - } - - /** - * Cancels queue for the provided download ID if exists. - * - * @param downloadId Download ID as known by [ExtensionManager] - */ - private fun cancelQueue(downloadId: Long) { - val waitingInstall = this.waitingInstall.get() - val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return - if (cancelEntry(toCancel)) { - queue.remove(toCancel) - if (waitingInstall == toCancel) { - // Currently processing removed entry, continue queue - this.waitingInstall.set(null) - checkQueue() - } - queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) } -// extensionManager.up(downloadId, InstallStep.Idle) - } - } - // Don't cancel if entry is already started installing - fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry - fun getActiveEntry(): Entry? = waitingInstall.get() + override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry - fun onDestroy() { + override fun onDestroy() { Shizuku.removeBinderDeadListener(shizukuDeadListener) Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener) ioScope.cancel() - LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver) - queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) } - queue.clear() - waitingInstall.set(null) + super.onDestroy() } private fun exec(command: String, stdin: InputStream? = null): ShellResult { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 981d973c41..d97ad8de5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -14,7 +14,7 @@ import androidx.core.net.toUri import co.touchlab.kermit.Logger import eu.kanade.tachiyomi.extension.ExtensionInstallerJob import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.extension.ShizukuInstaller +import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.util.storage.getUriCompat @@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.isPackageInstalled import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.toast +import java.io.File import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -47,7 +48,6 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.domain.base.BasePreferences -import java.io.File /** * The installer which installs, updates and uninstalls the extensions. diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index f7040a5c66..e8e9510f61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.DiskUtil -import eu.kanade.tachiyomi.util.storage.EpubFile -import eu.kanade.tachiyomi.util.storage.fillMetadata import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.extension import eu.kanade.tachiyomi.util.system.nameWithoutExtension @@ -33,7 +31,8 @@ import nl.adaptivity.xmlutil.serialization.XML import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -import yokai.core.archive.archiveReader +import yokai.core.archive.util.archiveReader +import yokai.core.archive.util.epubReader import yokai.core.metadata.COMIC_INFO_FILE import yokai.core.metadata.ComicInfo import yokai.core.metadata.copyFromComicInfo @@ -42,6 +41,7 @@ import yokai.domain.chapter.services.ChapterRecognition import yokai.domain.source.SourcePreferences import yokai.domain.storage.StorageManager import yokai.i18n.MR +import yokai.util.fillMetadata import yokai.util.lang.getString class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource { @@ -410,7 +410,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour } } is Format.Epub -> { - EpubFile(format.file.archiveReader(context)).use { epub -> + format.file.epubReader(context).use { epub -> val entry = epub.getImagesFromPages().firstOrNull() entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) } @@ -433,7 +433,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour true } is Format.Epub -> { - EpubFile(format.file.archiveReader(context)).use { epub -> + format.file.epubReader(context).use { epub -> epub.fillMetadata(chapter, manga) } true diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 407aa4a491..b503491fc7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -1,20 +1,13 @@ package eu.kanade.tachiyomi.source import android.content.Context -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.all.Cubari -import eu.kanade.tachiyomi.source.online.all.MangaDex -import eu.kanade.tachiyomi.source.online.english.KireiCake -import eu.kanade.tachiyomi.source.online.english.MangaPlus +import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -24,7 +17,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.util.concurrent.ConcurrentHashMap +import yokai.i18n.MR +import yokai.util.lang.getString class SourceManager( private val context: Context, @@ -40,28 +34,8 @@ class SourceManager( val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } val onlineSources: Flow> = catalogueSources.map { it.filterIsInstance() } - private val delegatedSources = listOf( - DelegatedSource( - "reader.kireicake.com", - 5509224355268673176, - KireiCake(), - ), - DelegatedSource( - "mangadex.org", - 2499283573021220255, - MangaDex(), - ), - DelegatedSource( - "mangaplus.shueisha.co.jp", - 1998944621602463790, - MangaPlus(), - ), - DelegatedSource( - "cubari.moe", - 6338219619148105941, - Cubari(), - ), - ).associateBy { it.sourceId } + // FIXME: Delegated source, unused at the moment, J2K only delegate deep links + private val delegatedSources = emptyList().associateBy { it.sourceId } init { scope.launch { @@ -71,8 +45,8 @@ class SourceManager( extensions.forEach { extension -> extension.sources.forEach { mutableMap[it.id] = it - delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource -// registerStubSource(it) + //delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource + //registerStubSource(it) } } sourcesMapFlow.value = mutableMap diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt deleted file mode 100644 index 1680498ce0..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt +++ /dev/null @@ -1,47 +0,0 @@ -package eu.kanade.tachiyomi.source.online - -import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.database.models.create -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.model.SChapter -import uy.kohesive.injekt.injectLazy -import yokai.domain.chapter.interactor.GetChapter -import yokai.domain.manga.interactor.GetManga - -abstract class DelegatedHttpSource { - - var delegate: HttpSource? = null - abstract val domainName: String - - protected val getChapter: GetChapter by injectLazy() - protected val getManga: GetManga by injectLazy() - - protected val network: NetworkHelper by injectLazy() - - abstract fun canOpenUrl(uri: Uri): Boolean - abstract fun chapterUrl(uri: Uri): String? - open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull() - abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? - - protected open suspend fun getMangaInfo(url: String): Manga? { - val id = delegate?.id ?: return null - val manga = Manga.create(url, "", id) - val networkManga = delegate?.getMangaDetails(manga.copy()) ?: return null - val newManga = MangaImpl().apply { - this.url = url - title = try { networkManga.title } catch (e: Exception) { "" } - source = id - } - newManga.copyFrom(networkManga) - return newManga - } - - suspend fun getChapters(url: String): List? { - val id = delegate?.id ?: return null - val manga = Manga.create(url, "", id) - return delegate?.getChapterList(manga) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt index 78816561fe..a0d0db6da4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt @@ -1,22 +1,31 @@ package eu.kanade.tachiyomi.source.online.all import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.chapter.interactor.GetChapter +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class Cubari : DelegatedHttpSource() { +class Cubari(delegate: HttpSource) : + DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + private val getChapter: GetChapter = Injekt.get() + + override val lang = "all" + override val domainName: String = "cubari" override fun canOpenUrl(uri: Uri): Boolean = true @@ -24,24 +33,24 @@ class Cubari : DelegatedHttpSource() { override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull() - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null val cubariPath = uri.pathSegments.getOrNull(2) ?: return null val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null val mangaUrl = "/read/$cubariType/$cubariPath" return withContext(Dispatchers.IO) { val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } val deferredChapters = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga -> + getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga -> val chapters = getChapter.awaitAll(manga, false) val chapter = findChapter(chapters, cubariType, chapterNumber) if (chapter != null) { return@async chapters } } - getChapters(mangaUrl) + getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() @@ -50,11 +59,7 @@ class Cubari : DelegatedHttpSource() { ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple(trueChapter, manga, chapters.orEmpty()) - } else { - null - } + Triple(trueChapter, manga, chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 15f011c18e..f87714d5d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -1,15 +1,15 @@ package eu.kanade.tachiyomi.source.online.all import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -20,10 +20,15 @@ import okhttp3.CacheControl import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class MangaDex : DelegatedHttpSource() { +class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + + override val lang: String = "all" override val domainName: String = "mangadex" @@ -42,13 +47,13 @@ class MangaDex : DelegatedHttpSource() { return uri.pathSegments.getOrNull(2)?.toIntOrNull() } - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val url = chapterUrl(uri) ?: return null val request = GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK) val response = network.client.newCall(request).await() if (response.code != 200) throw Exception("HTTP error ${response.code}") - val body = response.body.string().orEmpty() + val body = response.body.string() if (body.isEmpty()) { throw Exception("Null Response") } @@ -56,28 +61,19 @@ class MangaDex : DelegatedHttpSource() { val jsonObject = Json.decodeFromString(body) val dataObject = jsonObject.data ?: throw Exception("Chapter not found") val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter") - val langCode = getRealLangCode(dataObject.language ?: "en").uppercase(Locale.getDefault()) - // Use the correct MangaDex source based on the language code, or the api will not return - // the correct chapter list - delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" } - ?: error("Source not found") val mangaUrl = "/manga/$mangaId/" return withContext(Dispatchers.IO) { val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } - val deferredChapters = async { getChapters(mangaUrl) } + val deferredChapters = async { getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == "/api$url" }?.toChapter() ?: error( + val trueChapter = chapters.find { it.url == "/api$url" }?.toChapter() ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple(trueChapter, manga, chapters.orEmpty()) - } else { - null - } + Triple(trueChapter, manga, chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt deleted file mode 100644 index adeec2e93c..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt +++ /dev/null @@ -1,110 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.database.models.toChapter -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.online.DelegatedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext -import okhttp3.FormBody -import okhttp3.Request -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import yokai.i18n.MR -import yokai.util.lang.getString - -open class FoolSlide(override val domainName: String, private val urlModifier: String = "") : - DelegatedHttpSource - () { - - override fun canOpenUrl(uri: Uri): Boolean = true - - override fun chapterUrl(uri: Uri): String? { - val offset = if (urlModifier.isEmpty()) 0 else 1 - val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null - val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null - val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null - val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null - val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString() - return "$urlModifier/read/" + listOfNotNull( - mangaName, - lang, - volume, - chapterNumber, - subChapterNumber, - ).joinToString("/") + "/" - } - - override fun pageNumber(uri: Uri): Int? { - val count = uri.pathSegments.count() - if (count > 2 && uri.pathSegments[count - 2] == "page") { - return super.pageNumber(uri) - } - return null - } - - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { - val offset = if (urlModifier.isEmpty()) 0 else 1 - val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null - var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null - val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull() - if (subChapterNumber != null) { - chapterNumber += ".$subChapterNumber" - } - return withContext(Dispatchers.IO) { - val mangaUrl = "$urlModifier/series/$mangaName/" - val sourceId = delegate?.id ?: return@withContext null - val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, sourceId) ?: getManga(mangaUrl) - } - val chapterUrl = chapterUrl(uri) - val deferredChapters = async { getChapters(mangaUrl) } - val manga = deferredManga.await() - val chapters = deferredChapters.await() - val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error( - context.getString(MR.strings.chapter_not_found), - ) - if (manga != null) Triple(trueChapter, manga, chapters) else null - } - } - - open suspend fun getManga(url: String): Manga? { - val request = GET("${delegate!!.baseUrl}$url") - val document = network.client.newCall(allowAdult(request)).await().asJsoup() - val mangaDetailsInfoSelector = "div.info" - val infoElement = document.select(mangaDetailsInfoSelector).first()?.text() ?: return null - return MangaImpl().apply { - this.url = url - source = delegate?.id ?: -1 - title = infoElement.substringAfter("Title:").substringBefore("Author:").trim() - author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim() - artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim() - description = infoElement.substringAfter("Synopsis:").trim() - thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim() - } - } - - /** - * Transform a GET request into a POST request that automatically authorizes all adult content - */ - private fun allowAdult(request: Request) = allowAdult(request.url.toString()) - - private fun allowAdult(url: String): Request { - return POST( - url, - body = FormBody.Builder() - .add("adult", "true") - .build(), - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt deleted file mode 100644 index ecad868775..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt +++ /dev/null @@ -1,28 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.util.asJsoup -import eu.kanade.tachiyomi.util.lang.capitalizeWords - -class KireiCake : FoolSlide("kireicake") { - - override suspend fun getManga(url: String): Manga? { - val request = GET("${delegate!!.baseUrl}$url") - val document = network.client.newCall(request).await().asJsoup() - val mangaDetailsInfoSelector = "div.info" - return MangaImpl().apply { - this.url = url - source = delegate?.id ?: -1 - title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first() - ?.ownText()?.substringAfter(":")?.trim() - ?: url.split("/").last().replace("_", " " + "").capitalizeWords() - description = - document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first() - ?.ownText()?.substringAfter(":") - thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src") - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt index 710b6e4793..ebcbc527ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt @@ -1,24 +1,31 @@ package eu.kanade.tachiyomi.source.online.english import android.net.Uri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.toChapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.DelegatedHttpSource +import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import okhttp3.CacheControl import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString -class MangaPlus : DelegatedHttpSource() { +class MangaPlus(delegate: HttpSource) : + DelegatedHttpSource(delegate) { + + private val getManga: GetManga = Injekt.get() + + override val lang: String get() = delegate.lang + override val domainName: String = "jumpg-webapi.tokyo-cdn" private val titleIdRegex = @@ -34,11 +41,11 @@ class MangaPlus : DelegatedHttpSource() { override fun pageNumber(uri: Uri): Int? = null - override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { + override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? { val url = chapterUrl(uri) ?: return null val request = GET( chapterUrlTemplate.replace("##", uri.pathSegments[1]), - delegate!!.headers, + delegate.headers, CacheControl.FORCE_NETWORK, ) return withContext(Dispatchers.IO) { @@ -53,26 +60,22 @@ class MangaPlus : DelegatedHttpSource() { val trimmedTitle = title.substring(0, title.length - 1) val mangaUrl = "#/titles/$titleId" val deferredManga = async { - getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl) + getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl) } - val deferredChapters = async { getChapters(mangaUrl) } + val deferredChapters = async { getChapterListByUrl(mangaUrl) } val manga = deferredManga.await() val chapters = deferredChapters.await() val context = Injekt.get().context - val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error( + val trueChapter = chapters.find { it.url == url }?.toChapter() ?: error( context.getString(MR.strings.chapter_not_found), ) - if (manga != null) { - Triple( - trueChapter, - manga.apply { - this.title = trimmedTitle - }, - chapters, - ) - } else { - null - } + Triple( + trueChapter, + manga.apply { + this.title = trimmedTitle + }, + chapters, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseComposeController.kt index 8f78c042ec..4ecd065463 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseComposeController.kt @@ -5,19 +5,26 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import eu.kanade.tachiyomi.util.compose.LocalBackPress +import eu.kanade.tachiyomi.util.compose.LocalDialogHostState +import yokai.domain.DialogHostState import yokai.presentation.theme.YokaiTheme abstract class BaseComposeController(bundle: Bundle? = null) : BaseController(bundle) { + override val shouldHideLegacyAppBar = true + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle? ): View { - hideLegacyAppBar() + setAppBarVisibility() return ComposeView(container.context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -25,8 +32,14 @@ abstract class BaseComposeController(bundle: Bundle? = null) : ) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { + val dialogHostState = remember { DialogHostState() } YokaiTheme { - ScreenContent() + CompositionLocalProvider( + LocalDialogHostState provides dialogHostState, + LocalBackPress provides router::handleBack, + ) { + ScreenContent() + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt index f378bed539..b1fcea81e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.cancel abstract class BaseController(bundle: Bundle? = null) : Controller(bundle), BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface { + abstract val shouldHideLegacyAppBar: Boolean + lateinit var viewScope: CoroutineScope var isDragging = false @@ -58,6 +60,10 @@ abstract class BaseController(bundle: Bundle? = null) : open fun onViewCreated(view: View) { } + internal fun setAppBarVisibility() { + if (shouldHideLegacyAppBar) hideLegacyAppBar() else showLegacyAppBar() + } + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { if (type.isEnter && !isControllerVisible) { view?.alpha = 0f diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseLegacyController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseLegacyController.kt index 996074df6f..e710deaf8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseLegacyController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseLegacyController.kt @@ -20,11 +20,13 @@ import eu.kanade.tachiyomi.util.view.isControllerVisible abstract class BaseLegacyController(bundle: Bundle? = null) : BaseController(bundle) { + override val shouldHideLegacyAppBar = false + lateinit var binding: VB val isBindingInitialized get() = this::binding.isInitialized override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { - showLegacyAppBar() + setAppBarVisibility() binding = createBinding(inflater) binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background) return binding.root diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/StateCoroutinePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/StateCoroutinePresenter.kt new file mode 100644 index 0000000000..e89ed20283 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/StateCoroutinePresenter.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.ui.base.presenter + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Presenter that mimic [cafe.adriel.voyager.core.model.StateScreenModel] for easier migration. + * Temporary class while we're migrating to Compose. + */ +abstract class StateCoroutinePresenter(initialState: S) : BaseCoroutinePresenter() { + + protected val mutableState: MutableStateFlow = MutableStateFlow(initialState) + val state: StateFlow = mutableState.asStateFlow() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt index ef9a074b29..0afa1b2af0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt @@ -5,6 +5,8 @@ import android.content.Context import android.util.AttributeSet import android.view.MenuItem import android.widget.LinearLayout +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FileDownloadOff import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.isInvisible import androidx.core.view.updateLayoutParams @@ -212,7 +214,7 @@ class DownloadBottomSheet @JvmOverloads constructor( setBottomSheet() if (presenter.downloadQueueState.value.isEmpty()) { binding.emptyView.show( - R.drawable.ic_download_off_24dp, + Icons.Filled.FileDownloadOff, MR.strings.nothing_is_downloading, ) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 271d9d1755..dc63b2db5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -13,15 +13,15 @@ import eu.kanade.tachiyomi.util.lang.removeArticles import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.timeSpanFromNow import eu.kanade.tachiyomi.util.system.withDefContext +import java.util.* import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.injectLazy -import yokai.domain.ui.UiPreferences -import yokai.i18n.MR -import yokai.util.lang.getString -import java.util.* import yokai.domain.category.interactor.GetCategories import yokai.domain.chapter.interactor.GetChapter import yokai.domain.history.interactor.GetHistory +import yokai.domain.ui.UiPreferences +import yokai.i18n.MR +import yokai.util.lang.getString /** * Adapter storing a list of manga in a certain category. @@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : */ fun indexOf(manga: Manga): Int { return currentItems.indexOfFirst { - if (it is LibraryItem) { - it.manga.id == manga.id + if (it is LibraryMangaItem) { + it.manga.manga.id == manga.id } else { false } @@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : */ fun allIndexOf(manga: Manga): List { return currentItems.mapIndexedNotNull { index, it -> - if (it is LibraryItem && it.manga.id == manga.id) { + if (it is LibraryMangaItem && it.manga.manga.id == manga.id) { index } else { null @@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : } else { val filteredManga = withDefContext { mangas.filter { it.filter(s) } } if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) { - val catId = mangas.firstOrNull()?.let { it.header?.catId ?: it.manga.category } + val catId = (mangas.firstOrNull() as? LibraryMangaItem)?.let { it.header?.catId ?: it.manga.category } val blankItem = catId?.let { controller.presenter.blankItem(it) } updateDataSet(blankItem ?: emptyList()) } else { @@ -173,6 +173,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : } isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank() setItemsPerCategoryMap() + notifyDataSetChanged() } private fun getFirstLetter(name: String): String { @@ -202,18 +203,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : vibrateOnCategoryChange(item.category.name) item.category.name } - is LibraryItem -> { - val text = if (item.manga.isBlank()) { - return item.header?.category?.name.orEmpty() - } else { + is LibraryPlaceholderItem -> { + item.header?.category?.name.orEmpty() + } + is LibraryMangaItem -> { + val text = when (getSort(position)) { LibrarySort.DragAndDrop -> { - if (item.header.category.isDynamic && item.manga.id != null) { + if (item.header.category.isDynamic && item.manga.manga.id != null) { // FIXME: Don't do blocking - val category = runBlocking { getCategories.awaitByMangaId(item.manga.id!!) }.firstOrNull()?.name + val category = runBlocking { getCategories.awaitByMangaId(item.manga.manga.id!!) }.firstOrNull()?.name category ?: context.getString(MR.strings.default_value) } else { - val title = item.manga.title + val title = item.manga.manga.title if (preferences.removeArticles().get()) { title.removeArticles().chop(15) } else { @@ -222,14 +224,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : } } LibrarySort.DateFetched -> { - val id = item.manga.id ?: return "" + val id = item.manga.manga.id ?: return "" // FIXME: Don't do blocking val history = runBlocking { getChapter.awaitAll(id, false) } val last = history.maxOfOrNull { it.date_fetch } context.timeSpanFromNow(MR.strings.fetched_, last ?: 0) } LibrarySort.LastRead -> { - val id = item.manga.id ?: return "" + val id = item.manga.manga.id ?: return "" // FIXME: Don't do blocking val history = runBlocking { getHistory.awaitAllByMangaId(id) } val last = history.maxOfOrNull { it.last_read } @@ -256,21 +258,23 @@ class LibraryCategoryAdapter(val controller: LibraryController?) : } } LibrarySort.LatestChapter -> { - context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update) + context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update) } LibrarySort.DateAdded -> { - context.timeSpanFromNow(MR.strings.added_, item.manga.date_added) + context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added) } LibrarySort.Title -> { val title = if (preferences.removeArticles().get()) { - item.manga.title.removeArticles() + item.manga.manga.title.removeArticles() } else { - item.manga.title + item.manga.manga.title } getFirstLetter(title) } + LibrarySort.Random -> { + context.getString(MR.strings.random) + } } - } if (!isSingleCategory) { vibrateOnCategoryChange(item.header?.category?.name.orEmpty()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 2493614196..00d07100e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -28,6 +28,8 @@ import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HeartBroken import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.animation.doOnEnd import androidx.core.view.ViewCompat @@ -449,7 +451,7 @@ open class LibraryController( private fun setActiveCategory() { val currentCategory = presenter.categories.indexOfFirst { - if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategory == it.id + if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategoryId == it.id } if (currentCategory > -1) { binding.categoryRecycler.setCategories(currentCategory) @@ -519,14 +521,13 @@ open class LibraryController( } private fun openRandomManga(global: Boolean) { - val items = if (global) { - presenter.allLibraryItems - } else { - adapter.currentItems - }.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) } + val items = + if (global) { presenter.currentLibraryItems } else { adapter.currentItems } + .filterIsInstance() + .filter { !it.manga.manga.initialized || it.manga.unread > 0 } if (items.isNotEmpty()) { - val item = items.random() as LibraryItem - openManga(item.manga) + val item = items.random() as LibraryMangaItem + openManga(item.manga.manga) } } @@ -557,7 +558,7 @@ open class LibraryController( } presenter.groupType = item shouldScrollToTop = true - presenter.getLibrary() + presenter.updateLibrary() true }.show() } @@ -660,7 +661,7 @@ open class LibraryController( createActionModeIfNeeded() } - if (presenter.libraryItems.isNotEmpty() && !isSubClass) { + if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) { presenter.restoreLibrary() if (justStarted) { val activityBinding = activityBinding ?: return @@ -704,7 +705,7 @@ open class LibraryController( if (!LibraryUpdateJob.isRunning(context)) { when { !presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> { - presenter.findCurrentCategory()?.let { + presenter.currentCategory?.let { updateLibrary(it) } } @@ -902,7 +903,7 @@ open class LibraryController( } } else { val newOffset = - presenter.categories.indexOfFirst { presenter.currentCategory == it.id } + + presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } + (if (next) 1 else -1) if (if (!next) { newOffset > -1 @@ -1011,7 +1012,7 @@ open class LibraryController( override fun getSpanSize(position: Int): Int { if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount val item = this@LibraryController.mAdapter?.getItem(position) - return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) { + return if (item is LibraryHeaderItem || item is SearchGlobalItem || item is LibraryPlaceholderItem) { managerSpanCount } else { 1 @@ -1056,7 +1057,7 @@ open class LibraryController( if (type.isEnter) { binding.filterBottomSheet.filterBottomSheet.isVisible = true if (type == ControllerChangeType.POP_ENTER) { - presenter.getLibrary() + presenter.updateLibrary() isPoppingIn = true } binding.recyclerCover.isClickable = false @@ -1095,7 +1096,7 @@ open class LibraryController( if (!isBindingInitialized) return updateFilterSheetY() if (observeLater) { - presenter.getLibrary() + presenter.updateLibrary() } } @@ -1135,7 +1136,7 @@ open class LibraryController( binding.emptyView.hide() } else { binding.emptyView.show( - R.drawable.ic_heart_off_24dp, + Icons.Filled.HeartBroken, if (hasActiveFilters) { MR.strings.no_matches_for_filters } else { @@ -1374,7 +1375,7 @@ open class LibraryController( setActiveCategory() return } - val headerPosition = adapter.indexOf(pos) + val headerPosition = mAdapter?.indexOf(pos) ?: return if (headerPosition > -1) { val activityBinding = activityBinding ?: return val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition)) @@ -1408,7 +1409,7 @@ open class LibraryController( private fun onRefresh() { showCategories(false) - presenter.getLibrary() + presenter.updateLibrary() destroyActionModeIfNeeded() } @@ -1432,14 +1433,14 @@ open class LibraryController( val isShowAllCategoriesSet = preferences.showAllCategories().get() if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) { presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get() - presenter.getLibrary() + presenter.updateLibrary() } else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) { if (!isSubClass) { preferences.showAllCategoriesWhenSearchingSingleCategory() .set(presenter.forceShowAllCategories) } presenter.forceShowAllCategories = false - presenter.getLibrary() + presenter.updateLibrary() } if (query != this.query && !query.isNullOrBlank()) { @@ -1457,7 +1458,7 @@ open class LibraryController( adapter.removeAllScrollableHeaders() } adapter.setFilter(query) - if (presenter.allLibraryItems.isEmpty()) return true + if (presenter.currentLibraryItems.isEmpty()) return true viewScope.launchUI { adapter.performFilterAsync() } @@ -1476,7 +1477,6 @@ open class LibraryController( } private fun setSelection(manga: Manga, selected: Boolean) { - if (manga.isBlank()) return val currentMode = adapter.mode if (selected) { if (selectedMangas.add(manga)) { @@ -1526,7 +1526,7 @@ open class LibraryController( toggleSelection(position) return } - val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return + val manga = (adapter.getItem(position) as? LibraryMangaItem)?.manga?.manga ?: return val activity = activity ?: return val chapter = presenter.getFirstUnread(manga) ?: return activity.apply { @@ -1542,9 +1542,8 @@ open class LibraryController( } private fun toggleSelection(position: Int) { - val item = adapter.getItem(position) as? LibraryItem ?: return - if (item.manga.isBlank()) return - setSelection(item.manga, !adapter.isSelected(position)) + val item = adapter.getItem(position) as? LibraryMangaItem ?: return + setSelection(item.manga.manga, !adapter.isSelected(position)) invalidateActionMode() } @@ -1560,14 +1559,14 @@ open class LibraryController( * @return true if the item should be selected, false otherwise. */ override fun onItemClick(view: View?, position: Int): Boolean { - val item = adapter.getItem(position) as? LibraryItem ?: return false + val item = adapter.getItem(position) as? LibraryMangaItem ?: return false return if (adapter.mode == SelectableAdapter.Mode.MULTI) { snack?.dismiss() lastClickPosition = position toggleSelection(position) false } else { - openManga(item.manga) + openManga(item.manga.manga) false } } @@ -1589,10 +1588,10 @@ open class LibraryController( */ override fun onItemLongClick(position: Int) { val item = adapter.getItem(position) - if (item !is LibraryItem) return + if (item !is LibraryMangaItem) return snack?.dismiss() if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) { - snack = view?.snack(item.manga.title) { + snack = view?.snack(item.manga.manga.title) { anchorView = activityBinding?.bottomNav view.elevation = 15f.dpToPx } @@ -1641,14 +1640,14 @@ open class LibraryController( if (mangaId == null) { adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) } } else { - presenter.updateManga() + presenter.updateLibrary() } } private fun setSelection(position: Int, selected: Boolean = true) { - val item = adapter.getItem(position) as? LibraryItem ?: return + val item = adapter.getItem(position) as? LibraryMangaItem ?: return - setSelection(item.manga, selected) + setSelection(item.manga.manga, selected) invalidateActionMode() } @@ -1672,7 +1671,7 @@ open class LibraryController( override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition) - val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false + val item = adapter.getItem(fromPosition) as? LibraryMangaItem ?: return false val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem if (toPosition < 1) return false return (adapter.getItem(toPosition) !is LibraryHeaderItem) && ( @@ -1687,18 +1686,18 @@ open class LibraryController( lastItem = null isDragging = false binding.swipeRefresh.isEnabled = true - if (adapter.selectedItemCount > 0) { + if (mAdapter == null || adapter.selectedItemCount > 0) { lastItemPosition = null return } destroyActionModeIfNeeded() // if nothing moved if (lastItemPosition == null) return - val item = adapter.getItem(position) as? LibraryItem ?: return + val item = adapter.getItem(position) as? LibraryMangaItem ?: return val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem val libraryItems = getSectionItems(adapter.getSectionHeader(position), item) - .filterIsInstance() - val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id } + .filterIsInstance() + val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id } if (newHeader?.category?.id == item.manga.category) { presenter.rearrangeCategory(item.manga.category, mangaIds) } else { @@ -1820,7 +1819,7 @@ open class LibraryController( val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return if (!category.isDynamic) { ManageCategoryDialog(category) { - presenter.getLibrary() + presenter.updateLibrary() }.showDialog(router) } } @@ -1830,8 +1829,8 @@ open class LibraryController( if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) { val item = adapter.findCategoryHeader(catId) ?: return val libraryItems = adapter.getSectionItems(item) - .filterIsInstance() - val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id } + .filterIsInstance() + val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id } presenter.rearrangeCategory(catId, mangaIds) } else { presenter.sortCategory(catId, sortBy) @@ -1913,7 +1912,7 @@ open class LibraryController( isGone = true setOnClickListener { presenter.forceShowAllCategories = !presenter.forceShowAllCategories - presenter.getLibrary() + presenter.updateLibrary() isSelected = presenter.forceShowAllCategories } val pad = 12.dpToPx @@ -2193,7 +2192,7 @@ open class LibraryController( val activity = activity ?: return viewScope.launchIO { selectedMangas.toList().moveCategories(activity) { - presenter.getLibrary() + presenter.updateLibrary() destroyActionModeIfNeeded() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index 091642038b..86287493a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga /** * Class used to hold the displayed data of a manga in the library, like the cover or the title. @@ -64,23 +64,24 @@ class LibraryGridHolder( * @param item the manga item to bind. */ override fun onSetValues(item: LibraryItem) { + if (item !is LibraryMangaItem) throw IllegalStateException("Only LibraryMangaItem can use grid holder") // Update the title and subtitle of the manga. setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition" - binding.constraintLayout.isVisible = !item.manga.isBlank() - binding.title.text = item.manga.title.highlightText(item.filter, color) - binding.behindTitle.text = item.manga.title - val mangaColor = item.manga.dominantCoverColors + binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE + binding.title.text = item.manga.manga.title.highlightText(item.filter, color) + binding.behindTitle.text = item.manga.manga.title + val mangaColor = item.manga.manga.dominantCoverColors binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background) binding.behindTitle.setTextColor( mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground), ) - val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) { - item.manga.author?.trim() ?: "" + val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) { + item.manga.manga.author?.trim() ?: "" } else { listOfNotNull( - item.manga.author?.trim()?.takeIf { it.isNotBlank() }, - item.manga.artist?.trim()?.takeIf { it.isNotBlank() }, + item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() }, + item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() }, ).joinToString(", ") } binding.subtitle.text = authorArtist.highlightText(item.filter, color) @@ -101,7 +102,7 @@ class LibraryGridHolder( // Update the cover. binding.coverThumbnail.dispose() - setCover(item.manga) + setCover(item.manga.manga) } override fun toggleActivation() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt index 9ef0bd1db5..85be9b426a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt @@ -32,14 +32,17 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.compatToolTipText import eu.kanade.tachiyomi.util.view.setText import eu.kanade.tachiyomi.util.view.text +import kotlin.random.Random import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.library.LibraryPreferences import yokai.i18n.MR import yokai.util.lang.getString class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : BaseFlexibleViewHolder(view, adapter, true) { + private val libraryPreferences: LibraryPreferences = Injekt.get() private val binding = LibraryCategoryHeaderItemBinding.bind(view) val progressDrawableStart = CircularProgressDrawable(itemView.context) val progressDrawableEnd = CircularProgressDrawable(itemView.context) @@ -183,7 +186,17 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : binding.categoryTitle.text = categoryName + if (adapter.showNumber) { - " (${adapter.itemsPerCategory[item.catId]})" + val filteredCount = adapter.currentItems.count { + it is LibraryMangaItem && it.header?.catId == item.catId + } + val totalCount = adapter.itemsPerCategory[item.catId] ?: 0 + val searchText = adapter.getFilter(String::class.java) + var countText = if (searchText.isNullOrBlank()) { + " ($totalCount)" + } else { + " ($filteredCount/$totalCount)" + } + countText } else { "" } if (category.sourceId != null) { val icon = adapter.sourceManager.get(category.sourceId!!)?.icon() @@ -198,7 +211,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : val isAscending = category.isAscending() val sortingMode = category.sortingMode() - val sortDrawable = getSortRes(sortingMode, isAscending, R.drawable.ic_sort_24dp) + val sortDrawable = getSortRes(sortingMode, isAscending, category.isDynamic, false) binding.categorySort.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, sortDrawable, 0) binding.categorySort.setText(category.sortRes()) @@ -263,7 +276,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : val cat = category ?: return adapter.controller?.activity?.let { activity -> val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) } - val sortingMode = cat.sortingMode(true) + val sortingMode = cat.sortingMode() ?: if (!cat.isDynamic) LibrarySort.DragAndDrop else null val sheet = MaterialMenuSheet( activity, items, @@ -272,69 +285,68 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : ) { sheet, item -> onCatSortClicked(cat, item) val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category - val isAscending = nCategory?.isAscending() ?: false - val drawableRes = getSortRes(item, isAscending) - sheet.setDrawable(item, drawableRes) + sheet.updateSortIcon(nCategory, LibrarySort.valueOf(item)) false } - val isAscending = cat.isAscending() - val drawableRes = getSortRes(sortingMode, isAscending) - sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes) + sheet.updateSortIcon(cat, sortingMode) sheet.show() } } + private fun MaterialMenuSheet.updateSortIcon(category: Category?, sortingMode: LibrarySort?) { + val isAscending = category?.isAscending() ?: false + val drawableRes = getSortRes(sortingMode, isAscending, category?.isDynamic ?: false, true) + this.setDrawable(sortingMode?.mainValue ?: -1, drawableRes) + } + private fun getSortRes( sortMode: LibrarySort?, isAscending: Boolean, - @DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp, + isDynamic: Boolean, + onSelection: Boolean, + @DrawableRes defaultDrawableRes: Int = R.drawable.ic_sort_24dp, + @DrawableRes defaultSelectedDrawableRes: Int = R.drawable.ic_check_24dp, ): Int { - sortMode ?: return defaultDrawableRes - return when (sortMode) { - LibrarySort.DragAndDrop -> defaultDrawableRes - else -> { - if (if (sortMode.hasInvertedSort) !isAscending else isAscending) { - R.drawable.ic_arrow_downward_24dp - } else { - R.drawable.ic_arrow_upward_24dp - } - } - } - } + sortMode ?: return if (onSelection) defaultSelectedDrawableRes else defaultDrawableRes - private fun getSortRes( - sortingMode: Int?, - isAscending: Boolean, - @DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp, - ): Int { - sortingMode ?: return defaultDrawableRes - return when (val sortMode = LibrarySort.valueOf(sortingMode)) { - LibrarySort.DragAndDrop -> defaultDrawableRes - else -> { - if (if (sortMode?.hasInvertedSort == true) !isAscending else isAscending) { - R.drawable.ic_arrow_downward_24dp - } else { - R.drawable.ic_arrow_upward_24dp - } + if (sortMode.isDirectional) { + return if (if (sortMode.hasInvertedSort) !isAscending else isAscending) { + R.drawable.ic_arrow_downward_24dp + } else { + R.drawable.ic_arrow_upward_24dp } } + + if (onSelection) { + return when(sortMode) { + LibrarySort.DragAndDrop -> R.drawable.ic_check_24dp + LibrarySort.Random -> R.drawable.ic_refresh_24dp + else -> defaultSelectedDrawableRes + } + } + + return sortMode.iconRes(isDynamic) } private fun onCatSortClicked(category: Category, menuId: Int?) { - val modType = if (menuId == null) { + val (mode, modType) = if (menuId == null) { val sortingMode = category.sortingMode() ?: LibrarySort.Title - if (category.isAscending()) { - sortingMode.categoryValueDescending - } else { - sortingMode.categoryValue - } + sortingMode to + if (sortingMode != LibrarySort.Random && category.isAscending()) { + sortingMode.categoryValueDescending + } else { + sortingMode.categoryValue + } } else { val sortingMode = LibrarySort.valueOf(menuId) ?: LibrarySort.Title if (sortingMode != LibrarySort.DragAndDrop && sortingMode == category.sortingMode()) { onCatSortClicked(category, null) return } - sortingMode.categoryValue + sortingMode to sortingMode.categoryValue + } + if (mode == LibrarySort.Random) { + libraryPreferences.randomSortSeed().set(Random.nextInt()) } adapter.libraryListener?.sortCategory(category.id!!, modType) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 73e10f7fd9..5760a4808f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -5,9 +5,6 @@ import androidx.core.graphics.ColorUtils import androidx.core.view.isVisible import com.google.android.material.card.MaterialCardView import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.system.getResourceColor @@ -17,9 +14,7 @@ import eu.kanade.tachiyomi.util.view.setCards * Generic class used to hold the displayed data of a manga in the library. * @param view the inflated view for this holder. * @param adapter the adapter handling this holder. - * @param listener a listener to react to the single tap and long tap events. */ - abstract class LibraryHolder( view: View, val adapter: LibraryCategoryAdapter, @@ -43,7 +38,7 @@ abstract class LibraryHolder( */ abstract fun onSetValues(item: LibraryItem) - fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) { + fun setUnreadBadge(badge: LibraryBadge, item: LibraryMangaItem) { val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters badge.setUnreadDownload( when { @@ -54,7 +49,7 @@ abstract class LibraryHolder( }, when { item.downloadCount == -1 -> -1 - item.manga.isLocal() -> -2 + item.manga.manga.isLocal() -> -2 else -> item.downloadCount }, showTotal, @@ -63,7 +58,7 @@ abstract class LibraryHolder( ) } - fun setReadingButton(item: LibraryItem) { + fun setReadingButton(item: LibraryMangaItem) { itemView.findViewById(R.id.play_layout)?.isVisible = item.manga.unread > 0 && !item.hideReadingButton } @@ -80,8 +75,8 @@ abstract class LibraryHolder( override fun onLongClick(view: View?): Boolean { return if (adapter.isLongPressDragEnabled) { - val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga - if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) { + val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga + if (manga != null && !isDraggable) { adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition) toggleActivation() true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 956bca1be7..134e8241ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -1,205 +1,48 @@ package eu.kanade.tachiyomi.ui.library import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams +import androidx.annotation.CallSuper import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.IFilterable import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.data.database.models.seriesType import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.util.system.dpToPx -import eu.kanade.tachiyomi.util.view.compatToolTipText -import eu.kanade.tachiyomi.widget.AutofitRecyclerView import uy.kohesive.injekt.injectLazy import yokai.domain.ui.UiPreferences -class LibraryItem( - val manga: LibraryManga, +abstract class LibraryItem( header: LibraryHeaderItem, - private val context: Context?, + internal val context: Context?, ) : AbstractSectionableItem(header), IFilterable { - var downloadCount = -1 - var unreadType = 2 - var sourceLanguage: String? = null var filter = "" - private val sourceManager: SourceManager by injectLazy() + internal val sourceManager: SourceManager by injectLazy() private val uiPreferences: UiPreferences by injectLazy() private val preferences: PreferencesHelper by injectLazy() - private val uniformSize: Boolean + internal val uniformSize: Boolean get() = uiPreferences.uniformGrid().get() - private val libraryLayout: Int + internal val libraryLayout: Int get() = preferences.libraryLayout().get() val hideReadingButton: Boolean get() = preferences.hideStartReadingButton().get() - override fun getLayoutRes(): Int { - return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) { - R.layout.manga_list_item - } else { - R.layout.manga_grid_item - } - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { - val parent = adapter.recyclerView - return if (parent is AutofitRecyclerView) { - val libraryLayout = libraryLayout - val isFixedSize = uniformSize - if (libraryLayout == LAYOUT_LIST || manga.isBlank()) { - LibraryListHolder(view, adapter as LibraryCategoryAdapter) - } else { - view.apply { - val isStaggered = parent.layoutManager is StaggeredGridLayoutManager - val binding = MangaGridItemBinding.bind(this) - binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID - if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) { - binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID - binding.card.setCardForegroundColor( - ContextCompat.getColorStateList( - context, - R.color.library_comfortable_grid_foreground, - ), - ) - } - if (isFixedSize) { - binding.constraintLayout.layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - binding.coverThumbnail.maxHeight = Int.MAX_VALUE - binding.coverThumbnail.minimumHeight = 0 - binding.constraintLayout.minHeight = 0 - binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP - binding.coverThumbnail.adjustViewBounds = false - binding.coverThumbnail.updateLayoutParams { - height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT - dimensionRatio = "15:22" - } - } - if (libraryLayout != LAYOUT_COMFORTABLE_GRID) { - binding.card.updateLayoutParams { - bottomMargin = (if (isStaggered) 2 else 6).dpToPx - } - } - binding.setBGAndFG(libraryLayout) - } - val gridHolder = LibraryGridHolder( - view, - adapter as LibraryCategoryAdapter, - libraryLayout == LAYOUT_COMPACT_GRID, - isFixedSize, - ) - if (!isFixedSize) { - gridHolder.setFreeformCoverRatio(manga, parent) - } - gridHolder - } - } else { - LibraryListHolder(view, adapter as LibraryCategoryAdapter) - } - } - + @CallSuper override fun bindViewHolder( adapter: FlexibleAdapter>, holder: LibraryHolder, position: Int, payloads: MutableList?, ) { - if (holder is LibraryGridHolder && !holder.fixedSize) { - holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView) - } holder.onSetValues(this) (holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position)) - val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams - layoutParams?.isFullSpan = manga.isBlank() - if (libraryLayout == LAYOUT_COVER_ONLY_GRID) { - holder.itemView.compatToolTipText = manga.title - } - } - - /** - * Returns true if this item is draggable. - */ - override fun isDraggable(): Boolean { - return !manga.isBlank() && header.category.isDragAndDrop - } - - override fun isEnabled(): Boolean { - return !manga.isBlank() - } - - override fun isSelectable(): Boolean { - return !manga.isBlank() - } - - /** - * Filters a manga depending on a query. - * - * @param constraint the query to apply. - * @return true if the manga should be included, false otherwise. - */ - override fun filter(constraint: String): Boolean { - filter = constraint - if (manga.isBlank() && manga.title.isBlank()) { - return constraint.isEmpty() - } - val sourceName by lazy { sourceManager.getOrStub(manga.source).name } - return manga.title.contains(constraint, true) || - (manga.author?.contains(constraint, true) ?: false) || - (manga.artist?.contains(constraint, true) ?: false) || - sourceName.contains(constraint, true) || - if (constraint.contains(",")) { - val genres = manga.genre?.split(", ") - constraint.split(",").all { containsGenre(it.trim(), genres) } - } else { - containsGenre(constraint, manga.genre?.split(", ")) - } - } - - private fun containsGenre(tag: String, genres: List?): Boolean { - if (tag.trim().isEmpty()) return true - context ?: return false - val seriesType by lazy { manga.seriesType(context, sourceManager) } - return if (tag.startsWith("-")) { - val realTag = tag.substringAfter("-") - genres?.find { - it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true) - } == null - } else { - genres?.find { - it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true) - } != null - } - } - - override fun equals(other: Any?): Boolean { - if (other is LibraryItem) { - return manga.id == other.manga.id && manga.category == other.manga.category - } - return false - } - - override fun hashCode(): Int { - return 31 * manga.id!!.hashCode() + header!!.hashCode() + (holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index 568068a324..e3696c68e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.highlightText import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.setCards import yokai.i18n.MR -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga import yokai.util.lang.getString /** @@ -39,22 +39,25 @@ class LibraryListHolder( setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) binding.title.isVisible = true binding.constraintLayout.minHeight = 56.dpToPx - if (item.manga.isBlank()) { + if (item is LibraryPlaceholderItem) { binding.constraintLayout.minHeight = 0 binding.constraintLayout.updateLayoutParams { height = ViewGroup.MarginLayoutParams.WRAP_CONTENT } - if (item.manga.status == -1) { - binding.title.text = null - binding.title.isVisible = false - } else { - binding.title.text = itemView.context.getString( - if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) { - MR.strings.no_matches_for_filters_short - } else { - MR.strings.category_is_empty - }, - ) + when (item.type) { + is LibraryPlaceholderItem.Type.Blank -> { + binding.title.text = itemView.context.getString( + if (adapter.hasActiveFilters && item.type.mangaCount >= 1) { + MR.strings.no_matches_for_filters_short + } else { + MR.strings.category_is_empty + }, + ) + } + is LibraryPlaceholderItem.Type.Hidden -> { + binding.title.text = null + binding.title.isVisible = false + } } binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER binding.card.isVisible = false @@ -63,6 +66,9 @@ class LibraryListHolder( binding.subtitle.isVisible = false return } + + if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item") + binding.constraintLayout.updateLayoutParams { height = 52.dpToPx } @@ -71,16 +77,16 @@ class LibraryListHolder( binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START // Update the binding.title of the manga. - binding.title.text = item.manga.title.highlightText(item.filter, color) + binding.title.text = item.manga.manga.title.highlightText(item.filter, color) setUnreadBadge(binding.unreadDownloadBadge.badgeView, item) val authorArtist = - if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) { - item.manga.author?.trim() ?: "" + if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) { + item.manga.manga.author?.trim() ?: "" } else { listOfNotNull( - item.manga.author?.trim()?.takeIf { it.isNotBlank() }, - item.manga.artist?.trim()?.takeIf { it.isNotBlank() }, + item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() }, + item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() }, ).joinToString(", ") } @@ -95,7 +101,7 @@ class LibraryListHolder( // Update the cover. binding.coverThumbnail.dispose() - binding.coverThumbnail.loadManga(item.manga) + binding.coverThumbnail.loadManga(item.manga.manga) } override fun onActionStateChanged(position: Int, actionState: Int) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaItem.kt new file mode 100644 index 0000000000..28de0771f0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaItem.kt @@ -0,0 +1,180 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.data.database.models.seriesType +import eu.kanade.tachiyomi.databinding.MangaGridItemBinding +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.compatToolTipText +import eu.kanade.tachiyomi.widget.AutofitRecyclerView + +class LibraryMangaItem( + val manga: LibraryManga, + header: LibraryHeaderItem, + context: Context?, +) : LibraryItem(header, context) { + + var downloadCount = -1 + var unreadType = 2 + var sourceLanguage: String? = null + + override fun getLayoutRes(): Int { + return if (libraryLayout == LAYOUT_LIST) { + R.layout.manga_list_item + } else { + R.layout.manga_grid_item + } + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { + val listHolder by lazy { LibraryListHolder(view, adapter as LibraryCategoryAdapter) } + val parent = adapter.recyclerView + if (parent !is AutofitRecyclerView) return listHolder + + val libraryLayout = libraryLayout + val isFixedSize = uniformSize + + if (libraryLayout == LAYOUT_LIST) { return listHolder } + + view.apply { + val isStaggered = parent.layoutManager is StaggeredGridLayoutManager + val binding = MangaGridItemBinding.bind(this) + binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID + if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) { + binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID + binding.card.setCardForegroundColor( + ContextCompat.getColorStateList( + context, + R.color.library_comfortable_grid_foreground, + ), + ) + } + if (isFixedSize) { + binding.constraintLayout.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + binding.coverThumbnail.maxHeight = Int.MAX_VALUE + binding.coverThumbnail.minimumHeight = 0 + binding.constraintLayout.minHeight = 0 + binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP + binding.coverThumbnail.adjustViewBounds = false + binding.coverThumbnail.updateLayoutParams { + height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT + dimensionRatio = "2:3" + } + } + if (libraryLayout != LAYOUT_COMFORTABLE_GRID) { + binding.card.updateLayoutParams { + bottomMargin = (if (isStaggered) 2 else 6).dpToPx + } + } + binding.setBGAndFG(libraryLayout) + } + val gridHolder = LibraryGridHolder( + view, + adapter as LibraryCategoryAdapter, + libraryLayout == LAYOUT_COMPACT_GRID, + isFixedSize, + ) + if (!isFixedSize) { + gridHolder.setFreeformCoverRatio(manga.manga, parent) + } + return gridHolder + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: LibraryHolder, + position: Int, + payloads: MutableList?, + ) { + if (holder is LibraryGridHolder && !holder.fixedSize) { + holder.setFreeformCoverRatio(manga.manga, adapter.recyclerView as? AutofitRecyclerView) + } + super.bindViewHolder(adapter, holder, position, payloads) + if (libraryLayout == LAYOUT_COVER_ONLY_GRID) { + holder.itemView.compatToolTipText = manga.manga.title + } + } + + /** + * Returns true if this item is draggable. + */ + override fun isDraggable(): Boolean { + return header.category.isDragAndDrop + } + + override fun isEnabled(): Boolean { + return true + } + + override fun isSelectable(): Boolean { + return true + } + + /** + * Filters a manga depending on a query. + * + * @param constraint the query to apply. + * @return true if the manga should be included, false otherwise. + */ + override fun filter(constraint: String): Boolean { + filter = constraint + if (manga.manga.title.isBlank()) { + return constraint.isEmpty() + } + val sourceName by lazy { sourceManager.getOrStub(manga.manga.source).name } + return manga.manga.title.contains(constraint, true) || + (manga.manga.author?.contains(constraint, true) ?: false) || + (manga.manga.artist?.contains(constraint, true) ?: false) || + sourceName.contains(constraint, true) || + if (constraint.contains(",")) { + val genres = manga.manga.genre?.split(", ") + constraint.split(",").all { containsGenre(it.trim(), genres) } + } else { + containsGenre(constraint, manga.manga.genre?.split(", ")) + } + } + + private fun containsGenre(tag: String, genres: List?): Boolean { + if (tag.trim().isEmpty()) return true + context ?: return false + + val seriesType by lazy { manga.manga.seriesType(context, sourceManager) } + return if (tag.startsWith("-")) { + val realTag = tag.substringAfter("-") + genres?.find { + it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true) + } == null + } else { + genres?.find { + it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true) + } != null + } + } + + override fun equals(other: Any?): Boolean { + if (other is LibraryMangaItem) { + return manga.manga.id == other.manga.manga.id && manga.category == other.manga.category + } + return false + } + + override fun hashCode(): Int { + return 31 * manga.manga.id.hashCode() + header!!.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPlaceholderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPlaceholderItem.kt new file mode 100644 index 0000000000..d0b810a4a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPlaceholderItem.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R + +/** + * Placeholder item to indicate if the category is hidden or empty/filtered out. + */ +class LibraryPlaceholderItem ( + val category: Int, + val type: Type, + header: LibraryHeaderItem, + context: Context?, +) : LibraryItem(header, context) { + + override fun getLayoutRes(): Int = R.layout.manga_list_item + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LibraryHolder { + return LibraryListHolder(view, adapter as LibraryCategoryAdapter) + } + + override fun filter(constraint: String): Boolean { + filter = constraint + + if (type !is Type.Hidden || type.title.isBlank()) return constraint.isEmpty() + + return type.title.contains(constraint, true) + } + + override fun equals(other: Any?): Boolean { + if (other is LibraryPlaceholderItem) { + return category == other.category + } + return false + } + + override fun hashCode(): Int { + return 31 * Long.MIN_VALUE.hashCode() + header!!.hashCode() + } + + sealed class Type { + data class Hidden(val title: String, val hiddenItems: List) : Type() + data class Blank(val mangaCount: Int) : Type() + } + + companion object { + fun hidden(category: Int, header: LibraryHeaderItem, context: Context?, title: String, hiddenItems: List) = + LibraryPlaceholderItem(category, Type.Hidden(title, hiddenItems), header, context) + + fun blank(category: Int, header: LibraryHeaderItem, context: Context?, mangaCount: Int = 0) = + LibraryPlaceholderItem(category, Type.Blank(mangaCount), header, context) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 77ed4ca035..e82f372dcc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -4,12 +4,15 @@ import eu.kanade.tachiyomi.core.preference.minusAssign import eu.kanade.tachiyomi.core.preference.plusAssign import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Category.Companion.langSplitter +import eu.kanade.tachiyomi.data.database.models.Category.Companion.sourceSplitter import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter.Companion.copy import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.removeCover import eu.kanade.tachiyomi.data.database.models.seriesType +import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -51,14 +54,11 @@ import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.retry -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -73,6 +73,7 @@ import yokai.domain.chapter.interactor.GetChapter import yokai.domain.chapter.interactor.UpdateChapter import yokai.domain.chapter.models.ChapterUpdate import yokai.domain.history.interactor.GetHistory +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.GetLibraryManga import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.UpdateManga @@ -82,13 +83,18 @@ import yokai.i18n.MR import yokai.util.isLewd import yokai.util.lang.getString +typealias LibraryMap = Map> +typealias LibraryMutableMap = MutableMap> + /** * Presenter of [LibraryController]. */ class LibraryPresenter( private val preferences: PreferencesHelper = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), val sourceManager: SourceManager = Injekt.get(), + private val downloadCache: DownloadCache = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), @@ -103,10 +109,7 @@ class LibraryPresenter( private val getTrack: GetTrack by injectLazy() private val getHistory: GetHistory by injectLazy() - private val _fetchLibrary: Channel = Channel(Channel.UNLIMITED) - val fetchLibrary = _fetchLibrary.receiveAsFlow() - .onStart { emit(Unit) } - .shareIn(presenterScope, SharingStarted.Lazily, 1) + private val forceUpdateEvent: Channel = Channel(Channel.UNLIMITED) private val context = preferences.context private val viewContext @@ -123,18 +126,27 @@ class LibraryPresenter( var categories: List = emptyList() private set - private var removeArticles: Boolean = preferences.removeArticles().get() - /** All categories of the library, in case they are hidden because of hide categories is on */ private var allCategories: List = emptyList() - /** List of all manga to update the */ - var libraryItems: List = emptyList() - private var sectionedLibraryItems: MutableMap> = mutableMapOf() - var currentCategory = -1 + private var removeArticles: Boolean = preferences.removeArticles().get() + + /** List of all manga */ + var currentLibrary: LibraryMap = mapOf() private set - var allLibraryItems: List = emptyList() + val currentLibraryItems: List + get() = currentLibrary.values.flatten() + /** List of all manga to be displayed */ + private var libraryToDisplay: LibraryMutableMap = mutableMapOf() + val libraryItemsToDisplay: List + get() = libraryToDisplay.values.flatten() + + var currentCategoryId = -1 private set + var currentCategory: Category? + get() = allCategories.find { it.id == currentCategoryId } + set(value) { currentCategoryId = value?.id ?: 0 } + private var hiddenLibraryItems: List = emptyList() var forceShowAllCategories = false val showAllCategories @@ -171,16 +183,14 @@ class LibraryPresenter( fun isCategoryMoreThanOne(): Boolean = allCategories.size > 1 - fun findCurrentCategory() = allCategories.find { it.id == currentCategory } - /** Save the current list to speed up loading later */ override fun onDestroy() { val isSubController = controllerIsSubClass super.onDestroy() if (!isSubController) { - lastLibraryItems = libraryItems + lastDisplayedLibrary = libraryToDisplay lastCategories = categories - lastAllLibraryItems = allLibraryItems + lastLibrary = currentLibrary } } @@ -188,15 +198,16 @@ class LibraryPresenter( super.onCreate() if (!controllerIsSubClass) { - lastLibraryItems?.let { libraryItems = it } + lastDisplayedLibrary?.let { libraryToDisplay = it } lastCategories?.let { categories = it } - lastAllLibraryItems?.let { allLibraryItems = it } + lastLibrary?.let { currentLibrary = it } lastCategories = null - lastLibraryItems = null - lastAllLibraryItems = null + lastDisplayedLibrary = null + lastLibrary = null } subscribeLibrary() + updateLibrary() if (!preferences.showLibrarySearchSuggestions().isSet()) { DelayedLibrarySuggestionsJob.setupTask(context, true) @@ -212,12 +223,16 @@ class LibraryPresenter( } fun getItemCountInCategories(categoryId: Int): Int { - val items = sectionedLibraryItems[categoryId] - return if (items?.firstOrNull()?.manga?.isHidden() == true || items?.firstOrNull()?.manga?.isBlank() == true) { - items.firstOrNull()?.manga?.read ?: 0 - } else { - sectionedLibraryItems[categoryId]?.size ?: 0 + val category = categories.find { it.id == categoryId } + val items = libraryToDisplay[category] + val firstItem = items?.firstOrNull() as? LibraryPlaceholderItem? + if (firstItem != null) { + if (firstItem.type !is LibraryPlaceholderItem.Type.Hidden) { + return 0 + } + return firstItem.type.hiddenItems.size } + return items?.size ?: 0 } private fun subscribeLibrary() { @@ -233,38 +248,31 @@ class LibraryPresenter( combine( getLibraryFlow(), - fetchLibrary, - ) { data, _ -> + downloadCache.changes, + ) { data, _ -> data }.collectLatest { data -> categories = data.categories allCategories = data.allCategories - data.items - } - .collectLatest { library -> - val hiddenItems = library.filter { it.manga.isHidden() }.mapNotNull { it.manga.items }.flatten() + val library = data.items + val hiddenItems = data.hiddenItems - setDownloadCount(library) - setUnreadBadge(library) - setSourceLanguage(library) - setDownloadCount(hiddenItems) - setUnreadBadge(hiddenItems) - setSourceLanguage(hiddenItems) - - allLibraryItems = library - hiddenLibraryItems = hiddenItems - val mangaMap = library - .applyFilters() - .applySort() - val freshStart = libraryItems.isEmpty() - sectionLibrary(mangaMap, freshStart) + library.forEach { (_, items) -> + setDownloadCount(items) + setUnreadBadge(items) + setSourceLanguage(items) } - } - } + setDownloadCount(hiddenItems) + setUnreadBadge(hiddenItems) + setSourceLanguage(hiddenItems) - /** Get favorited manga for library and sort and filter it */ - fun getLibrary() { - presenterScope.launch { - _fetchLibrary.send(Unit) + currentLibrary = library + hiddenLibraryItems = hiddenItems + val mangaMap = library + .applyFilters() + .applySort() + val freshStart = libraryToDisplay.isEmpty() + sectionLibrary(mangaMap, freshStart) + } } } @@ -278,16 +286,16 @@ class LibraryPresenter( fun switchSection(order: Int) { preferences.lastUsedCategory().set(order) - val category = categories.find { it.order == order }?.id ?: return + val category = categories.find { it.order == order } ?: return currentCategory = category - view?.onNextLibraryUpdate(sectionedLibraryItems[currentCategory] ?: blankItem()) + view?.onNextLibraryUpdate(libraryToDisplay[category] ?: blankItem()) } - fun blankItem(id: Int = currentCategory, categories: List? = null): List { + fun blankItem(id: Int = currentCategoryId, categories: List? = null): List { val actualCategories = categories ?: this.categories return listOf( - LibraryItem( - LibraryManga.createBlank(id), + LibraryPlaceholderItem.blank( + id, LibraryHeaderItem({ actualCategories.getOrDefault(id) }, id), viewContext, ), @@ -295,20 +303,17 @@ class LibraryPresenter( } fun restoreLibrary() { - val items = libraryItems val show = showAllCategories || !libraryIsGrouped || categories.size == 1 - sectionedLibraryItems = items.groupBy { it.header.category.id!! }.toMutableMap() - if (!show && currentCategory == -1) { - currentCategory = categories.find { - it.order == preferences.lastUsedCategory().get() - }?.id ?: 0 + if (!show && currentCategoryId == -1) { + currentCategory = categories.find { it.order == preferences.lastUsedCategory().get() } } view?.onNextLibraryUpdate( if (!show) { - sectionedLibraryItems[currentCategory] - ?: sectionedLibraryItems[categories.first().id] ?: blankItem() + libraryToDisplay[currentCategory] + ?: libraryToDisplay[categories.first()] + ?: blankItem() } else { - libraryItems + libraryItemsToDisplay }, true, ) @@ -316,26 +321,29 @@ class LibraryPresenter( fun getMangaInCategories(catId: Int?): List? { catId ?: return null - return allLibraryItems.filter { it.header.category.id == catId }.map { it.manga } + return currentLibraryItems + .filterIsInstance() + .filter { it.header.category.id == catId } + .map { it.manga } } - private suspend fun sectionLibrary(items: List, freshStart: Boolean = false) { - libraryItems = items - val showAll = showAllCategories || !libraryIsGrouped || - categories.size <= 1 - sectionedLibraryItems = items.groupBy { it.header.category.id ?: 0 }.toMutableMap() - if (!showAll && currentCategory == -1) { - currentCategory = categories.find { - it.order == preferences.lastUsedCategory().get() - }?.id ?: 0 + private suspend fun sectionLibrary(items: LibraryMap, freshStart: Boolean = false) { + val showAll = showAllCategories || !libraryIsGrouped || categories.size <= 1 + + libraryToDisplay = items.toMutableMap() + + if (!showAll && currentCategoryId == -1) { + currentCategory = categories.find { it.order == preferences.lastUsedCategory().get() } } + withUIContext { view?.onNextLibraryUpdate( if (!showAll) { - sectionedLibraryItems[currentCategory] - ?: sectionedLibraryItems[categories.first().id] ?: blankItem() + libraryToDisplay[currentCategory] + ?: libraryToDisplay[categories.first()] + ?: blankItem() } else { - libraryItems + libraryItemsToDisplay }, freshStart, ) @@ -347,7 +355,7 @@ class LibraryPresenter( * * @param items the items to filter. */ - private suspend fun List.applyFilters(): List { + private suspend fun LibraryMap.applyFilters(): LibraryMap { val filterPrefs = getPreferencesFlow().first() val showEmptyCategoriesWhileFiltering = preferences.showEmptyCategoriesWhileFiltering().get() @@ -363,57 +371,68 @@ class LibraryPresenter( filterPrefs.filterContentType == 0 ) hasActiveFilters = !filtersOff - val missingCategorySet = categories.mapNotNull { it.id }.toMutableSet() val realCount = mutableMapOf() - val filteredItems = this.filter f@{ item -> + val filteredItems = this.mapValues { (key, items) -> if (showEmptyCategoriesWhileFiltering) { - realCount[item.manga.category] = sectionedLibraryItems[item.manga.category]?.size ?: 0 + realCount[key.id ?: 0] = libraryToDisplay[key]?.size ?: 0 } - if (!showEmptyCategoriesWhileFiltering && item.manga.isHidden()) { - val subItems = sectionedLibraryItems[item.manga.category]?.takeUnless { it.size <= 1 } - ?: hiddenLibraryItems.filter { it.manga.category == item.manga.category } - if (subItems.isEmpty()) { - return@f filtersOff - } else { - return@f subItems.any { - matchesFilters( - it, - filterPrefs, - filterTrackers, - ) + items.filter f@{ item -> + if (item is LibraryMangaItem) { + return@f matchesFilters( + item, + filterPrefs, + filterTrackers, + ) + } + + if ( + !showEmptyCategoriesWhileFiltering + && item is LibraryPlaceholderItem + && item.type is LibraryPlaceholderItem.Type.Hidden + ) { + val subItems = (libraryToDisplay[key] ?: hiddenLibraryItems) + .filterIsInstance() + .filter { it.manga.category == item.category } + if (subItems.isEmpty()) { + return@f filtersOff + } else { + return@f subItems.any { + matchesFilters( + it, + filterPrefs, + filterTrackers, + ) + } } } - } else if (item.manga.isBlank() || item.manga.isHidden()) { - missingCategorySet.remove(item.manga.category) - return@f if (showAllCategories) { + + if (showAllCategories) { filtersOff || showEmptyCategoriesWhileFiltering } else { true } + }.ifEmpty { + if (showEmptyCategoriesWhileFiltering) { + val catId = key.id!! + listOf( + LibraryPlaceholderItem.blank( + catId, + LibraryHeaderItem({ this@LibraryPresenter.categories.getOrDefault(catId) }, catId), + viewContext, + realCount[catId] ?: 0, + ), + ) + } else { + emptyList() + } } - val matches = matchesFilters( - item, - filterPrefs, - filterTrackers, - ) - if (matches) { - missingCategorySet.remove(item.manga.category) - } - matches - }.toMutableList() - if (showEmptyCategoriesWhileFiltering) { - missingCategorySet.forEach { - filteredItems.add( - blankItem(it).first().apply { manga.realMangaCount = realCount[it] ?: 0 } - ) - } - } + }.toMutableMap() return filteredItems } private suspend fun matchesFilters( - item: LibraryItem, + item: LibraryMangaItem, filterPrefs: ItemPreferences, filterTrackers: String, ): Boolean { @@ -433,9 +452,9 @@ class LibraryPresenter( if (filterPrefs.filterMangaType > 0) { if (if (filterPrefs.filterMangaType == Manga.TYPE_MANHWA) { - item.manga.seriesType(sourceManager = sourceManager) !in arrayOf(filterPrefs.filterMangaType, Manga.TYPE_WEBTOON) + item.manga.manga.seriesType(sourceManager = sourceManager) !in arrayOf(filterPrefs.filterMangaType, Manga.TYPE_WEBTOON) } else { - filterPrefs.filterMangaType != item.manga.seriesType(sourceManager = sourceManager) + filterPrefs.filterMangaType != item.manga.manga.seriesType(sourceManager = sourceManager) } ) { return false @@ -443,51 +462,51 @@ class LibraryPresenter( } // Filter for completed status of manga - if (filterPrefs.filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) return false - if (filterPrefs.filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) return false + if (filterPrefs.filterCompleted == STATE_INCLUDE && item.manga.manga.status != SManga.COMPLETED) return false + if (filterPrefs.filterCompleted == STATE_EXCLUDE && item.manga.manga.status == SManga.COMPLETED) return false if (!matchesFilterTracking(item, filterPrefs.filterTracked, filterTrackers)) return false // Filter for downloaded manga if (filterPrefs.filterDownloaded != STATE_IGNORE) { val isDownloaded = when { - item.manga.isLocal() -> true + item.manga.manga.isLocal() -> true item.downloadCount != -1 -> item.downloadCount > 0 - else -> downloadManager.getDownloadCount(item.manga) > 0 + else -> downloadManager.getDownloadCount(item.manga.manga) > 0 } return if (filterPrefs.filterDownloaded == STATE_INCLUDE) isDownloaded else !isDownloaded } // Filter for NSFW/SFW contents - if (filterPrefs.filterContentType == STATE_INCLUDE) return !item.manga.isLewd() - if (filterPrefs.filterContentType == STATE_EXCLUDE) return item.manga.isLewd() + if (filterPrefs.filterContentType == STATE_INCLUDE) return !item.manga.manga.isLewd() + if (filterPrefs.filterContentType == STATE_EXCLUDE) return item.manga.manga.isLewd() return true } private suspend fun matchesCustomFilters( - item: LibraryItem, + item: LibraryMangaItem, customFilters: FilteredLibraryController, filterTrackers: String, ): Boolean { val statuses = customFilters.filterStatus if (statuses.isNotEmpty()) { - if (item.manga.status !in statuses) return false + if (item.manga.manga.status !in statuses) return false } val seriesTypes = customFilters.filterMangaType if (seriesTypes.isNotEmpty()) { - if (item.manga.seriesType(sourceManager = sourceManager) !in seriesTypes) return false + if (item.manga.manga.seriesType(sourceManager = sourceManager) !in seriesTypes) return false } val languages = customFilters.filterLanguages if (languages.isNotEmpty()) { - if (getLanguage(item.manga) !in languages) return false + if (getLanguage(item.manga.manga) !in languages) return false } val sources = customFilters.filterSources if (sources.isNotEmpty()) { - if (item.manga.source !in sources) return false + if (item.manga.manga.source !in sources) return false } val trackingScore = customFilters.filterTrackingScore if (trackingScore > 0 || trackingScore == -1) { - val tracks = getTrack.awaitAllByMangaId(item.manga.id!!) + val tracks = getTrack.awaitAllByMangaId(item.manga.manga.id!!) val hasTrack = loggedServices.any { service -> tracks.any { it.sync_id == service.id } @@ -512,7 +531,7 @@ class LibraryPresenter( } val tags = customFilters.filterTags if (tags.isNotEmpty()) { - val genres = item.manga.getGenres() ?: return false + val genres = item.manga.manga.getGenres() ?: return false if (tags.none { tag -> genres.any { it.equals(tag, true) } }) return false } return true @@ -528,8 +547,8 @@ class LibraryPresenter( } private suspend fun LibraryManga.getStartYear(): Int { - if (getChapter.awaitAll(id!!, false).any { it.read }) { - val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 } + if (getChapter.awaitAll(manga.id!!, false).any { it.read }) { + val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 } val date = chapters.minOfOrNull { it.last_read } ?: return -1 val cal = Calendar.getInstance().apply { timeInMillis = date } return if (date <= 0L) -1 else cal.get(Calendar.YEAR) @@ -546,13 +565,13 @@ class LibraryPresenter( } private suspend fun matchesFilterTracking( - item: LibraryItem, + item: LibraryMangaItem, filterTracked: Int, filterTrackers: String, ): Boolean { // Filter for tracked (or per tracked service) if (filterTracked != STATE_IGNORE) { - val tracks = getTrack.awaitAllByMangaId(item.manga.id!!) + val tracks = getTrack.awaitAllByMangaId(item.manga.manga.id!!) val hasTrack = loggedServices.any { service -> tracks.any { it.sync_id == service.id } @@ -595,19 +614,22 @@ class LibraryPresenter( if (!preferences.downloadBadge().get()) { // Unset download count if the preference is not enabled. for (item in itemList) { + if (item !is LibraryMangaItem) continue item.downloadCount = -1 } return } for (item in itemList) { - item.downloadCount = downloadManager.getDownloadCount(item.manga) + if (item !is LibraryMangaItem) continue + item.downloadCount = downloadManager.getDownloadCount(item.manga.manga) } } private fun setUnreadBadge(itemList: List) { val unreadType = preferences.unreadBadgeType().get() for (item in itemList) { + if (item !is LibraryMangaItem) continue item.unreadType = unreadType } } @@ -615,7 +637,8 @@ class LibraryPresenter( private fun setSourceLanguage(itemList: List) { val showLanguageBadges = preferences.languageBadge().get() for (item in itemList) { - item.sourceLanguage = if (showLanguageBadges) getLanguage(item.manga) else null + if (item !is LibraryMangaItem) continue + item.sourceLanguage = if (showLanguageBadges) getLanguage(item.manga.manga) else null } } @@ -636,88 +659,113 @@ class LibraryPresenter( * * @param itemList the map to sort. */ - private fun List.applySort(): List { + private fun LibraryMap.applySort(): LibraryMap { + // Making sure `allCategories` is stable for `.sort()` + val categoryOrderMap = allCategories.associate { it.id to it.order } + val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - if (i1.header.category.id == i2.header.category.id) { - val category = i1.header.category - if (category.mangaOrder.isEmpty() && category.mangaSort == null) { - category.changeSortTo(preferences.librarySortingMode().get()) - if (category.id == 0) { - preferences.defaultMangaOrder() - .set(category.mangaSort.toString()) - } else if (!category.isDynamic) { - onCategoryUpdate( - CategoryUpdate( - id = category.id!!.toLong(), - mangaOrder = category.mangaOrderToString(), - ) - ) - } - } - val compare = when { - category.mangaSort != null -> { - var sort = when (category.sortingMode() ?: LibrarySort.Title) { - LibrarySort.Title -> sortAlphabetical(i1, i2) - LibrarySort.LatestChapter -> i2.manga.latestUpdate.compareTo(i1.manga.latestUpdate) - LibrarySort.Unread -> when { - i1.manga.unread == i2.manga.unread -> 0 - i1.manga.unread == 0 -> if (category.isAscending()) 1 else -1 - i2.manga.unread == 0 -> if (category.isAscending()) -1 else 1 - else -> i1.manga.unread.compareTo(i2.manga.unread) - } - LibrarySort.LastRead -> { - i1.manga.lastRead.compareTo(i2.manga.lastRead) - } - LibrarySort.TotalChapters -> { - i1.manga.totalChapters.compareTo(i2.manga.totalChapters) - } - LibrarySort.DateFetched -> { - i1.manga.lastFetch.compareTo(i2.manga.lastFetch) - } - LibrarySort.DateAdded -> i2.manga.date_added.compareTo(i1.manga.date_added) - LibrarySort.DragAndDrop -> { - if (category.isDynamic) { - val category1 = - allCategories.find { i1.manga.category == it.id }?.order - ?: 0 - val category2 = - allCategories.find { i2.manga.category == it.id }?.order - ?: 0 - category1.compareTo(category2) - } else { - sortAlphabetical(i1, i2) - } + val category = i1.header.category + val compare = when { + i1 is LibraryPlaceholderItem -> -1 + i2 is LibraryPlaceholderItem -> 1 + i1 !is LibraryMangaItem || i2 !is LibraryMangaItem -> 0 + category.mangaSort != null -> { + var sort = when (category.sortingMode() ?: LibrarySort.Title) { + LibrarySort.Title -> sortAlphabetical(i1, i2) + LibrarySort.LatestChapter -> i2.manga.latestUpdate.compareTo(i1.manga.latestUpdate) + LibrarySort.Unread -> when { + i1.manga.unread == i2.manga.unread -> 0 + i1.manga.unread == 0 -> if (category.isAscending()) 1 else -1 + i2.manga.unread == 0 -> if (category.isAscending()) -1 else 1 + else -> i1.manga.unread.compareTo(i2.manga.unread) + } + LibrarySort.LastRead -> { + i1.manga.lastRead.compareTo(i2.manga.lastRead) + } + LibrarySort.TotalChapters -> { + i1.manga.totalChapters.compareTo(i2.manga.totalChapters) + } + LibrarySort.DateFetched -> { + i1.manga.lastFetch.compareTo(i2.manga.lastFetch) + } + LibrarySort.DateAdded -> i2.manga.manga.date_added.compareTo(i1.manga.manga.date_added) + LibrarySort.DragAndDrop -> { + if (category.isDynamic) { + val category1 = categoryOrderMap[i1.manga.category] ?: 0 + val category2 = categoryOrderMap[i2.manga.category] ?: 0 + category1.compareTo(category2) + } else { + sortAlphabetical(i1, i2) } } - if (!category.isAscending()) sort *= -1 - sort - } - category.mangaOrder.isNotEmpty() -> { - val order = category.mangaOrder - val index1 = order.indexOf(i1.manga.id!!) - val index2 = order.indexOf(i2.manga.id!!) - when { - index1 == index2 -> 0 - index1 == -1 -> -1 - index2 == -1 -> 1 - else -> index1.compareTo(index2) + LibrarySort.Random -> { + error("You're not supposed to be here...") } } - else -> 0 + if (!category.isAscending()) sort *= -1 + sort } - if (compare == 0) { - sortAlphabetical(i1, i2) - } else { - compare + category.mangaOrder.isNotEmpty() -> { + val order = category.mangaOrder + val index1 = order.indexOf(i1.manga.manga.id!!) + val index2 = order.indexOf(i2.manga.manga.id!!) + when { + index1 == index2 -> 0 + index1 == -1 -> -1 + index2 == -1 -> 1 + else -> index1.compareTo(index2) + } } + else -> 0 + } + if (compare == 0 && i1 is LibraryMangaItem && i2 is LibraryMangaItem) { + sortAlphabetical(i1, i2) } else { - val category = i1.header.category.order - val category2 = i2.header.category.order - category.compareTo(category2) + compare } } - return this.sortedWith(Comparator(sortFn)) + return this.mapValues { (category, values) -> + // Making sure category has valid sort before doing the actual sorting + if (category.mangaOrder.isEmpty() && category.mangaSort == null) { + category.changeSortTo(preferences.librarySortingMode().get()) + if (category.id == 0) { + preferences.defaultMangaOrder() + .set(category.mangaSort.toString()) + } else if (!category.isDynamic) { + onCategoryUpdate( + CategoryUpdate( + id = category.id!!.toLong(), + mangaOrder = category.mangaOrderToString(), + ) + ) + } + } + + if (LibrarySort.valueOf(category.mangaSort) == LibrarySort.Random) { + return@mapValues values + .asSequence() + .shuffled(Random(libraryPreferences.randomSortSeed().get())) + .sortedWith { i1, i2 -> + when { + i1 is LibraryPlaceholderItem -> -1 + i2 is LibraryPlaceholderItem -> 1 + else -> 0 + } + } + .toList() + } + + values.sortedWith(Comparator(sortFn)) + }.toSortedMap { category, category2 -> + when { + // Force default category to always be at the top. This also for some reason fixed a bug where Default + // category would disappear whenever a new category is added. + category.id == 0 -> -1 + category2.id == 0 -> 1 + else -> category.order.compareTo(category2.order) + } + } } /** Gets the category by id @@ -736,11 +784,11 @@ class LibraryPresenter( * @param i1 the first manga * @param i2 the second manga to compare */ - private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int { + private fun sortAlphabetical(i1: LibraryMangaItem, i2: LibraryMangaItem): Int { return if (removeArticles) { - i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true) + i1.manga.manga.title.removeArticles().compareTo(i2.manga.manga.title.removeArticles(), true) } else { - i1.manga.title.compareTo(i2.manga.title, true) + i1.manga.manga.title.compareTo(i2.manga.manga.title, true) } } @@ -755,9 +803,12 @@ class LibraryPresenter( preferences.groupLibraryBy().changes(), preferences.showAllCategories().changes(), - + preferences.librarySortingMode().changes(), preferences.librarySortingAscending().changes(), + + preferences.collapsedCategories().changes(), + preferences.collapsedDynamicCategories().changes(), ) { ItemPreferences( filterDownloaded = it[0] as Int, @@ -771,36 +822,38 @@ class LibraryPresenter( showAllCategories = it[8] as Boolean, sortingMode = it[9] as Int, sortAscending = it[10] as Boolean, + collapsedCategories = it[11] as Set, + collapsedDynamicCategories = it[12] as Set, ) } - private fun MutableList.addRemovedManga( - removedManga: Map>, - ): MutableList { - removedManga.keys.forEach { key -> - val manga = removedManga[key] ?: return@forEach - val headerItem = try { - manga.first().header - } catch (e: NoSuchElementException) { - return@forEach // No hidden manga to be handled - } - val mergedTitle = manga.joinToString("-") { - it.manga.title + "-" + it.manga.author - } - this.add( - LibraryItem( - LibraryManga.createHide( - headerItem.catId, - mergedTitle, - manga, - ), - headerItem, - viewContext, - ), - ) - } - return this - } +// private fun MutableList.addRemovedManga( +// removedManga: Map>, +// ): MutableList { +// removedManga.keys.forEach { key -> +// val manga = removedManga[key] ?: return@forEach +// val headerItem = try { +// manga.first().header +// } catch (e: NoSuchElementException) { +// return@forEach // No hidden manga to be handled +// } +// val mergedTitle = manga.joinToString("-") { +// it.manga.title + "-" + it.manga.manga.author +// } +// this.add( +// LibraryItem( +// LibraryManga.createHide( +// headerItem.catId, +// mergedTitle, +// manga, +// ), +// headerItem, +// viewContext, +// ), +// ) +// } +// return this +// } /** * Library's flow. @@ -808,26 +861,26 @@ class LibraryPresenter( * If category id '-1' is not empty, it means the library not grouped by categories */ private fun getLibraryFlow(): Flow { - return combine( + val libraryFlow = combine( getCategories.subscribe(), // FIXME: Remove retry once a real solution is found getLibraryManga.subscribe().retry(1) { e -> e is NullPointerException }, getPreferencesFlow(), - preferences.removeArticles().changes(), - fetchLibrary - ) { dbCategories, libraryMangaList, prefs, removeArticles, _ -> + forceUpdateEvent.receiveAsFlow(), + ) { dbCategories, libraryMangaList, prefs, _ -> groupType = prefs.groupType val defaultCategory = createDefaultCategory() - val allCategories = listOf(defaultCategory) + dbCategories - val (items, categories, hiddenItems) = if (groupType <= BY_DEFAULT || !libraryIsGrouped) { + // FIXME: Should return Map where Int is category id + if (groupType <= BY_DEFAULT || !libraryIsGrouped) { getLibraryItems( dbCategories, libraryMangaList, prefs.sortingMode, prefs.sortAscending, prefs.showAllCategories, + prefs.collapsedCategories, defaultCategory, ) } else { @@ -836,8 +889,17 @@ class LibraryPresenter( prefs.sortingMode, prefs.sortAscending, groupType, + prefs.collapsedDynamicCategories, ) - } + } to listOf(defaultCategory) + dbCategories + } + + return combine( + libraryFlow, + preferences.removeArticles().changes(), + ) { library, removeArticles -> + val (libraryItems, allCategories) = library + val (items, categories, hiddenItems) = libraryItems LibraryData( categories = categories, @@ -850,14 +912,15 @@ class LibraryPresenter( } private fun getLibraryItems( - allCategories: List, + dbCategories: List, libraryManga: List, sortingMode: Int, isAscending: Boolean, showAll: Boolean, + collapsedCategories: Set, defaultCategory: Category, - ): Triple, List, List> { - val categories = allCategories.toMutableList() + ): Triple, List> { + val categories = dbCategories.mapNotNull { if (it.id == null) null else it }.toMutableList() val hiddenItems = mutableListOf() val categoryAll = Category.createAll( @@ -866,85 +929,81 @@ class LibraryPresenter( isAscending, ) val catItemAll = LibraryHeaderItem({ categoryAll }, -1) - val categorySet = mutableSetOf() - val headerItems = ( - categories.mapNotNull { category -> - val id = category.id - if (id == null) { - null - } else { - id to LibraryHeaderItem({ categories.getOrDefault(id) }, id) - } - } + (-1 to catItemAll) + (0 to LibraryHeaderItem({ categories.getOrDefault(0) }, 0)) - ).toMap() - val items = if (libraryIsGrouped) { - libraryManga - } else { - libraryManga.distinctBy { it.id } - }.mapNotNull { - val headerItem = ( - if (!libraryIsGrouped) { - catItemAll - } else { - headerItems[it.category] - } - ) ?: return@mapNotNull null - categorySet.add(it.category) - LibraryItem(it, headerItem, viewContext) - }.toMutableList() + // NOTE: Don't call header.category, only header.catId + val headerItems = ( + categories.map { category -> + val id = category.id!! + id to LibraryHeaderItem({ this@LibraryPresenter.categories.getOrDefault(id) }, id) + } + (0 to LibraryHeaderItem({ this@LibraryPresenter.categories.getOrDefault(0) }, 0)) + ).toMap() val categoriesHidden = if (forceShowAllCategories || controllerIsSubClass) { emptySet() } else { - preferences.collapsedCategories().get().mapNotNull { it.toIntOrNull() }.toSet() + collapsedCategories.mapNotNull { it.toIntOrNull() }.toSet() } - if (categorySet.contains(0)) categories.add(0, defaultCategory) - if (libraryIsGrouped) { - categories.forEach { category -> - val catId = category.id ?: return@forEach - if (catId > 0 && !categorySet.contains(catId) && - (catId !in categoriesHidden || !showAll) - ) { - val headerItem = headerItems[catId] - if (headerItem != null) { - items.add( - LibraryItem(LibraryManga.createBlank(catId), headerItem, viewContext), + val map = if (!libraryIsGrouped) + libraryManga + .asSequence() + .distinctBy { it.manga.id } + .map { LibraryMangaItem(it, catItemAll, viewContext) } + .groupBy { categoryAll } + else { + val rt = libraryManga + .asSequence() + .mapNotNull { + val headerItem = headerItems[it.category] ?: return@mapNotNull null + LibraryMangaItem(it, headerItem, viewContext) + } + .groupBy { it.header.catId } + + // Only show default category when needed + if (rt.containsKey(0)) categories.add(0, defaultCategory) + + // NOTE: Empty list means hide the category entirely + categories + .associateWith { rt[it.id].orEmpty() } + .mapValues { (key, values) -> + val catId = key.id!! // null check already handled by mapNotNull + val headerItem = headerItems[catId]!! // null check already handled by mapNotNull + + // Hide category if "Show all categories" is enabled and there's more than 1 category + if (catId in categoriesHidden && showAll && categories.size > 1) { + val mergedTitle = values.joinToString("-") { + it.manga.manga.title + "-" + it.manga.manga.author + } + libraryToDisplay[key] = values + hiddenItems.addAll(values) + return@mapValues listOf( + LibraryPlaceholderItem.hidden( + catId, + headerItem, + viewContext, + mergedTitle, + values, + ), ) } - } else if (catId in categoriesHidden && showAll && categories.size > 1) { - val mangaToRemove = items.filter { it.manga.category == catId } - val mergedTitle = mangaToRemove.joinToString("-") { - it.manga.title + "-" + it.manga.author - } - sectionedLibraryItems[catId] = mangaToRemove - hiddenItems.addAll(mangaToRemove) - items.removeAll(mangaToRemove) - val headerItem = headerItems[catId] - if (headerItem != null) { - items.add( - LibraryItem( - LibraryManga.createHide( - catId, - mergedTitle, - mangaToRemove, - ), + + // Making sure empty category is shown properly + values.ifEmpty { + listOf( + LibraryPlaceholderItem.blank( + catId, headerItem, viewContext, ), ) } } - } - } + }.toMutableMap() - categories.forEach { - it.isHidden = it.id in categoriesHidden && showAll && categories.size > 1 - } + categories.forEach { it.isHidden = it.id in categoriesHidden && showAll && categories.size > 1 } return Triple( - items, + map, if (!libraryIsGrouped) { arrayListOf(categoryAll) } else { @@ -959,12 +1018,14 @@ class LibraryPresenter( sortingMode: Int, isAscending: Boolean, groupType: Int, - ): Triple, List, List> { + collapsedDynamicCategories: Set, + ): Triple, List> { val tagItems: MutableMap = mutableMapOf() + val hiddenItems = mutableListOf() // internal function to make headers fun makeOrGetHeader(name: String, checkNameSwap: Boolean = false): LibraryHeaderItem { - tagItems.get(name)?.let { return it } + tagItems[name]?.let { return it } if (checkNameSwap && name.contains(" ")) { val swappedName = name.split(" ").reversed().joinToString(" ") if (tagItems.containsKey(swappedName)) { @@ -976,26 +1037,32 @@ class LibraryPresenter( return headerItem } + val hiddenDynamics = if (controllerIsSubClass) { + emptySet() + } else { + collapsedDynamicCategories + } + val unknown = context.getString(MR.strings.unknown) - val items = libraryManga.distinctBy { it.id }.map { manga -> + val items = libraryManga.distinctBy { it.manga.id }.map { manga -> when (groupType) { BY_TAG -> { - val tags = if (manga.genre.isNullOrBlank()) { + val tags = if (manga.manga.genre.isNullOrBlank()) { listOf(unknown) } else { - manga.genre?.split(",")?.mapNotNull { + manga.manga.genre?.split(",")?.mapNotNull { val tag = it.trim().capitalizeWords() tag.ifBlank { null } } ?: listOf(unknown) } tags.map { - LibraryItem(manga, makeOrGetHeader(it), viewContext) + LibraryMangaItem(manga, makeOrGetHeader(it), viewContext) } } BY_TRACK_STATUS -> { - val tracks = getTrack.awaitAllByMangaId(manga.id!!) + val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!) val track = tracks.find { track -> - loggedServices.any { it.id == track?.sync_id } + loggedServices.any { it.id == track.sync_id } } val service = loggedServices.find { it.id == track?.sync_id } val status: String = if (track != null && service != null) { @@ -1007,12 +1074,12 @@ class LibraryPresenter( } else { view?.view?.context?.getString(MR.strings.not_tracked) ?: "" } - listOf(LibraryItem(manga, makeOrGetHeader(status), viewContext)) + listOf(LibraryMangaItem(manga, makeOrGetHeader(status), viewContext)) } BY_SOURCE -> { - val source = sourceManager.getOrStub(manga.source) + val source = sourceManager.getOrStub(manga.manga.source) listOf( - LibraryItem( + LibraryMangaItem( manga, makeOrGetHeader("${source.name}$sourceSplitter${source.id}"), viewContext, @@ -1020,26 +1087,26 @@ class LibraryPresenter( ) } BY_AUTHOR -> { - if (manga.artist.isNullOrBlank() && manga.author.isNullOrBlank()) { - listOf(LibraryItem(manga, makeOrGetHeader(unknown), viewContext)) + if (manga.manga.artist.isNullOrBlank() && manga.manga.author.isNullOrBlank()) { + listOf(LibraryMangaItem(manga, makeOrGetHeader(unknown), viewContext)) } else { listOfNotNull( - manga.author.takeUnless { it.isNullOrBlank() }, - manga.artist.takeUnless { it.isNullOrBlank() }, + manga.manga.author.takeUnless { it.isNullOrBlank() }, + manga.manga.artist.takeUnless { it.isNullOrBlank() }, ).map { it.split(",", "/", " x ", " - ", ignoreCase = true).mapNotNull { name -> val author = name.trim() author.ifBlank { null } } }.flatten().distinct().map { - LibraryItem(manga, makeOrGetHeader(it, true), viewContext) + LibraryMangaItem(manga, makeOrGetHeader(it, true), viewContext) } } } BY_LANGUAGE -> { - val lang = getLanguage(manga) + val lang = getLanguage(manga.manga) listOf( - LibraryItem( + LibraryMangaItem( manga, makeOrGetHeader( lang?.plus(langSplitter)?.plus( @@ -1054,15 +1121,11 @@ class LibraryPresenter( ), ) } - else -> listOf(LibraryItem(manga, makeOrGetHeader(context.mapStatus(manga.status)), viewContext)) // BY_STATUS + // BY_STATUS + else -> listOf(LibraryMangaItem(manga, makeOrGetHeader(context.mapStatus(manga.manga.status)), viewContext)) } - }.flatten().toMutableList() + }.flatten().groupBy { it.header.catId } - val hiddenDynamics = if (controllerIsSubClass) { - emptySet() - } else { - preferences.collapsedDynamicCategories().get() - } val headers = tagItems.map { item -> Category.createCustom( item.key, @@ -1093,37 +1156,35 @@ class LibraryPresenter( if (!preferences.collapsedDynamicAtBottom().get()) return@let headers headers.filterNot { it.isHidden } + headers.filter { it.isHidden } } - headers.forEach { category -> - val catId = category.id ?: return@forEach - val headerItem = - tagItems[ - when { - category.sourceId != null -> "${category.name}$sourceSplitter${category.sourceId}" - category.langId != null -> "${category.langId}$langSplitter${category.name}" - else -> category.name - }, - ] - if (category.isHidden) { - val mangaToRemove = items.filter { it.header.catId == catId } - val mergedTitle = mangaToRemove.joinToString("-") { - it.manga.title + "-" + it.manga.author - } - sectionedLibraryItems[catId] = mangaToRemove - items.removeAll { it.header.catId == catId } - if (headerItem != null) { - items.add( - LibraryItem( - LibraryManga.createHide(catId, mergedTitle, mangaToRemove), - headerItem, - viewContext, - ), - ) + + val map = headers + .associateWith { items[it.id].orEmpty() } + .mapValues { (key, values) -> + val catId = key.id!! // null check already handled by mapNotNull + val headerItem = tagItems[key.dynamicHeaderKey()] + if (key.isHidden) { + val mergedTitle = values.joinToString("-") { + it.manga.manga.title + "-" + it.manga.manga.author + } + libraryToDisplay[key] = values + hiddenItems.addAll(values) + if (headerItem != null) { + return@mapValues listOf( + LibraryPlaceholderItem.hidden( + catId, + headerItem, + viewContext, + mergedTitle, + values, + ), + ) + } } + values } - } headers.forEachIndexed { index, category -> category.order = index } - return Triple(items, headers, listOf()) + return Triple(map, headers, hiddenItems) } private fun mapTrackingOrder(status: String): String { @@ -1156,7 +1217,7 @@ class LibraryPresenter( /** Requests the library to be filtered. */ fun requestFilterUpdate() { presenterScope.launch { - val mangaMap = allLibraryItems + val mangaMap = currentLibrary .applyFilters() .applySort() sectionLibrary(mangaMap) @@ -1165,11 +1226,11 @@ class LibraryPresenter( private fun requestBadgeUpdate(badgeUpdate: (List) -> Unit) { presenterScope.launch { - val mangaMap = allLibraryItems - badgeUpdate(mangaMap) - allLibraryItems = mangaMap - val current = libraryItems - badgeUpdate(current) + val mangaMap = currentLibrary + mangaMap.forEach { (_, items) -> badgeUpdate(items) } + currentLibrary = mangaMap + val current = libraryToDisplay + current.forEach { (_, items) -> badgeUpdate(items) } sectionLibrary(current) } } @@ -1192,7 +1253,7 @@ class LibraryPresenter( /** Requests the library to be sorted. */ private fun requestSortUpdate() { presenterScope.launch { - val mangaMap = libraryItems + val mangaMap = libraryToDisplay .applySort() sectionLibrary(mangaMap) } @@ -1217,7 +1278,6 @@ class LibraryPresenter( .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = false) else null } withIOContext { updateManga.awaitAll(mangaToDelete) } - getLibrary() } } @@ -1240,8 +1300,11 @@ class LibraryPresenter( } } - /** Called when Library Service updates a manga, update the item as well */ - fun updateManga() = getLibrary() + /** Force update the library */ + fun updateLibrary() = presenterScope.launch { + forceUpdateEvent.send(Unit) + } + /** Undo the removal of the manga once in library */ fun reAddMangas(mangas: List) { @@ -1251,12 +1314,12 @@ class LibraryPresenter( withIOContext { updateManga.awaitAll(mangaToAdd) } (view as? FilteredLibraryController)?.updateStatsPage() - getLibrary() } } /** Returns first unread chapter of a manga */ fun getFirstUnread(manga: Manga): Chapter? { + // FIXME: Don't do blocking val chapters = runBlocking { getChapter.awaitAll(manga) } return ChapterSort(manga, chapterFilter, preferences).getNextUnreadChapter(chapters, false) } @@ -1308,7 +1371,6 @@ class LibraryPresenter( } } - // TODO: Use SQLDelight /** Shift a manga's category via drag & drop */ fun moveMangaToCategory( manga: LibraryManga, @@ -1328,7 +1390,7 @@ class LibraryPresenter( if (catId == 0) { emptyList() } else { - getCategories.awaitByMangaId(manga.id!!) + getCategories.awaitByMangaId(manga.manga.id!!) .filter { it.id != oldCatId } + listOf(category) } @@ -1336,11 +1398,11 @@ class LibraryPresenter( mc.add(cat.id!!.toLong()) } - setMangaCategories.await(manga.id!!, mc) + setMangaCategories.await(manga.manga.id!!, mc) if (category.mangaSort == null) { val ids = mangaIds.toMutableList() - if (!ids.contains(manga.id!!)) ids.add(manga.id!!) + if (!ids.contains(manga.manga.id!!)) ids.add(manga.manga.id!!) category.mangaOrder = ids if (category.id == 0) { preferences.defaultMangaOrder() @@ -1354,14 +1416,14 @@ class LibraryPresenter( ) } } - getLibrary() + updateLibrary() } } /** Returns if manga is in a category by id */ fun mangaIsInCategory(manga: LibraryManga, catId: Int?): Boolean { // FIXME: Don't do blocking - val categories = runBlocking { getCategories.awaitByMangaId(manga.id!!) }.map { it.id } + val categories = runBlocking { getCategories.awaitByMangaId(manga.manga.id!!) }.map { it.id } return catId in categories } @@ -1389,7 +1451,6 @@ class LibraryPresenter( } preferences.collapsedDynamicCategories().set(categoriesHidden) } - getLibrary() } private fun getDynamicCategoryName(category: Category): String = @@ -1420,7 +1481,6 @@ class LibraryPresenter( } } } - getLibrary() } fun allCategoriesExpanded(): Boolean { @@ -1462,7 +1522,7 @@ class LibraryPresenter( mapMangaChapters[manga] = chapters } - getLibrary() + updateLibrary() } return mapMangaChapters } @@ -1478,7 +1538,7 @@ class LibraryPresenter( } }.flatten() updateChapter.awaitAll(updates) - getLibrary() + updateLibrary() } } @@ -1503,11 +1563,9 @@ class LibraryPresenter( } companion object { - private var lastLibraryItems: List? = null + private var lastDisplayedLibrary: LibraryMutableMap? = null private var lastCategories: List? = null - private var lastAllLibraryItems: List? = null - private const val sourceSplitter = "◘•◘" - private const val langSplitter = "⨼⨦⨠" + private var lastLibrary: LibraryMap? = null private const val dynamicCategorySplitter = "▄╪\t▄╪\t▄" private val randomTags = arrayOf(0, 1, 2) @@ -1523,9 +1581,9 @@ class LibraryPresenter( private const val randomGroupOfTagsNegate = 2 fun onLowMemory() { - lastLibraryItems = null + lastDisplayedLibrary = null lastCategories = null - lastAllLibraryItems = null + lastLibrary = null } suspend fun setSearchSuggestion( @@ -1545,15 +1603,15 @@ class LibraryPresenter( preferences.librarySearchSuggestion().set( when (val value = random.nextInt(0, 5)) { randomSource -> { - val distinctSources = getLibraryManga.await().distinctBy { it.source } + val distinctSources = getLibraryManga.await().distinctBy { it.manga.source } val randomSource = sourceManager.get( - distinctSources.randomOrNull(random)?.source ?: 0L, + distinctSources.randomOrNull(random)?.manga?.source ?: 0L, )?.name randomSource?.chopByWords(30) } randomTitle -> { - getLibraryManga.await().randomOrNull(random)?.title?.chopByWords(30) + getLibraryManga.await().randomOrNull(random)?.manga?.title?.chopByWords(30) } in randomTags -> { val tags = RecentsPresenter.getRecentManga(true) @@ -1597,11 +1655,11 @@ class LibraryPresenter( ) { val libraryManga = getLibraryManga.await() libraryManga.forEach { manga -> - if (manga.id == null) return@forEach - if (manga.date_added == 0L) { - val chapters = getChapter.awaitAll(manga) - manga.date_added = chapters.minByOrNull { it.date_fetch }?.date_fetch ?: 0L - updateManga.await(MangaUpdate(manga.id!!, dateAdded = manga.date_added)) + if (manga.manga.id == null) return@forEach + if (manga.manga.date_added == 0L) { + val chapters = getChapter.awaitAll(manga.manga.id!!, manga.manga.filtered_scanlators?.isNotBlank() == true) + manga.manga.date_added = chapters.minByOrNull { it.date_fetch }?.date_fetch ?: 0L + updateManga.await(MangaUpdate(manga.manga.id!!, dateAdded = manga.manga.date_added)) } } } @@ -1623,15 +1681,15 @@ class LibraryPresenter( val getLibraryManga: GetLibraryManga by injectLazy() val libraryManga = getLibraryManga.await() libraryManga.forEach { manga -> - if (manga.id == null) return@forEach - if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { - val file = cc.getCoverFile(manga.thumbnail_url, !manga.favorite) + if (manga.manga.id == null) return@forEach + if (manga.manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { + val file = cc.getCoverFile(manga.manga.thumbnail_url, !manga.manga.favorite) if (file != null && file.exists()) { - file.renameTo(cc.getCustomCoverFile(manga)) + file.renameTo(cc.getCustomCoverFile(manga.manga)) } - manga.thumbnail_url = - manga.thumbnail_url!!.lowercase(Locale.ROOT).substringAfter("custom-") - updateManga.await(MangaUpdate(manga.id!!, thumbnailUrl = manga.thumbnail_url)) + manga.manga.thumbnail_url = + manga.manga.thumbnail_url!!.lowercase(Locale.ROOT).substringAfter("custom-") + updateManga.await(MangaUpdate(manga.manga.id!!, thumbnailUrl = manga.manga.thumbnail_url)) } } } @@ -1651,12 +1709,15 @@ class LibraryPresenter( val sortingMode: Int, val sortAscending: Boolean, + + val collapsedCategories: Set, + val collapsedDynamicCategories: Set, ) data class LibraryData( val categories: List, val allCategories: List, - val items: List, + val items: LibraryMap, val hiddenItems: List, val removeArticles: Boolean, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt index 0d5ae1dd7d..460906a65b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -1,13 +1,10 @@ package eu.kanade.tachiyomi.ui.library import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet +import yokai.i18n.MR enum class LibrarySort( val mainValue: Int, @@ -33,7 +30,11 @@ enum class LibrarySort( MR.strings.category, R.drawable.ic_label_outline_24dp, ), - + Random( + 8, + MR.strings.random, + R.drawable.ic_shuffle_24dp, + ), ; val categoryValue: Char @@ -50,6 +51,7 @@ enum class LibrarySort( LatestChapter -> "LATEST_CHAPTER" DateFetched -> "CHAPTER_FETCH_DATE" DateAdded -> "DATE_ADDED" + Random -> "RANDOM" else -> "ALPHABETICAL" } return "$type,ASCENDING" @@ -63,6 +65,9 @@ enum class LibrarySort( val hasInvertedSort: Boolean get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched) + val isDirectional: Boolean + get() = this !in listOf(DragAndDrop, Random) + fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem { return MaterialMenuSheet.MenuSheetItem( mainValue, @@ -85,6 +90,7 @@ enum class LibrarySort( "LATEST_CHAPTER" -> LatestChapter "CHAPTER_FETCH_DATE" -> DateFetched "DATE_ADDED" -> DateAdded + "RANDOM" -> Random else -> Title } } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposeController.kt new file mode 100644 index 0000000000..ac75e11f02 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposeController.kt @@ -0,0 +1,93 @@ +package eu.kanade.tachiyomi.ui.library.compose + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.core.view.isGone +import androidx.core.view.isVisible +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.databinding.LibraryControllerBinding +import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController +import eu.kanade.tachiyomi.ui.library.models.LibraryItem +import eu.kanade.tachiyomi.ui.main.BottomSheetController +import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface +import eu.kanade.tachiyomi.ui.main.RootSearchInterface +import java.util.Locale +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import yokai.domain.ui.UiPreferences +import yokai.i18n.MR +import yokai.presentation.library.LibraryContent +import yokai.presentation.theme.YokaiTheme +import yokai.util.lang.getString + +class LibraryComposeController( + bundle: Bundle? = null, + val uiPreferences: UiPreferences = Injekt.get(), + val preferences: PreferencesHelper = Injekt.get(), +) : BaseCoroutineController(bundle) , + BottomSheetController, + RootSearchInterface, + FloatingSearchInterface { + + override fun getTitle(): String? { + return view?.context?.getString(MR.strings.library) + } + + override fun getSearchTitle(): String? { + val searchSuggestion by lazy { preferences.librarySearchSuggestion().get() } + + return searchTitle( + if (preferences.showLibrarySearchSuggestions().get() && searchSuggestion.isNotBlank()) { + "\"$searchSuggestion\"" + } else { + view?.context?.getString(MR.strings.your_library)?.lowercase(Locale.ROOT) + }, + ) + } + + override val presenter = LibraryComposePresenter() + + override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater) + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + binding.composeView.isVisible = true + binding.swipeRefresh.isGone = true + binding.fastScroller.isGone = true + + binding.composeView.setContent { + YokaiTheme { + ScreenContent() + } + } + } + + @Composable + fun ScreenContent() { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + + val state by presenter.state.collectAsState() + LibraryContent( + modifier = Modifier.nestedScroll(nestedScrollInterop), + items = (0..50).map { LibraryItem.Blank(it) }, + columns = 3, + ) + } + + override fun showSheet() { + } + + override fun hideSheet() { + } + + override fun toggleSheet() { + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposePresenter.kt new file mode 100644 index 0000000000..03461eeee5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/compose/LibraryComposePresenter.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.ui.library.compose + +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.ui.base.presenter.StateCoroutinePresenter +import eu.kanade.tachiyomi.ui.library.models.LibraryItem + +typealias LibraryMap = Map> + +class LibraryComposePresenter : + StateCoroutinePresenter(State()) { + + data class State( + var isLoading: Boolean = true, + var library: LibraryMap = emptyMap() + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryCategoryView.kt index b409856977..0dc927b9e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryCategoryView.kt @@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.library.display import android.content.Context import android.util.AttributeSet -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.databinding.LibraryCategoryLayoutBinding import eu.kanade.tachiyomi.util.bindToPreference import eu.kanade.tachiyomi.util.lang.withSubtitle import eu.kanade.tachiyomi.util.system.toInt import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView import kotlin.math.min +import yokai.i18n.MR +import yokai.util.lang.getString class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : BaseLibraryDisplayView(context, attrs) { @@ -20,7 +18,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att override fun initGeneralPreferences() { with(binding) { showAll.bindToPreference(preferences.showAllCategories()) { - controller?.presenter?.getLibrary() + controller?.presenter?.updateLibrary() binding.categoryShow.isEnabled = it } categoryShow.isEnabled = showAll.isChecked @@ -30,7 +28,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att dynamicToBottom.text = context.getString(MR.strings.move_dynamic_to_bottom) .withSubtitle(context, MR.strings.when_grouping_by_sources_tags) dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) { - controller?.presenter?.getLibrary() + controller?.presenter?.updateLibrary() } showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) { controller?.presenter?.requestFilterUpdate() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt index dbf17c3c49..94d55f515c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/filter/FilterBottomSheet.kt @@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryGroup +import eu.kanade.tachiyomi.ui.library.LibraryMangaItem import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.launchUI @@ -36,6 +37,8 @@ import eu.kanade.tachiyomi.util.view.isCollapsed import eu.kanade.tachiyomi.util.view.isExpanded import eu.kanade.tachiyomi.util.view.isHidden import eu.kanade.tachiyomi.util.view.setText +import kotlin.math.max +import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop @@ -48,8 +51,6 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.i18n.MR import yokai.util.lang.getString -import kotlin.math.max -import kotlin.math.roundToInt class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs), @@ -368,11 +369,12 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri suspend fun checkForManhwa(sourceManager: SourceManager) { if (checked) return withIOContext { - val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext + val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext checked = true var types = mutableSetOf() libraryManga.forEach { - when (it.manga.seriesType(sourceManager = sourceManager)) { + if (it !is LibraryMangaItem) return@forEach + when (it.manga.manga.seriesType(sourceManager = sourceManager)) { Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa) Manga.TYPE_MANHUA -> types.add(MR.strings.manhua) Manga.TYPE_COMIC -> types.add(MR.strings.comic) @@ -637,7 +639,7 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri SeriesType('m', MR.strings.series_type), Bookmarked('b', MR.strings.bookmarked), Tracked('t', MR.strings.tracking), - ContentType('s', MR.strings.content_type); + ContentType('s', MR.strings.content_type) ; companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/models/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/models/LibraryItem.kt new file mode 100644 index 0000000000..a952dae416 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/models/LibraryItem.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.ui.library.models + +import eu.kanade.tachiyomi.data.database.models.LibraryManga + +sealed interface LibraryItem { + data class Blank(val mangaCount: Int = 0) : LibraryItem + data class Hidden(val title: String, val hiddenItems: List) : LibraryItem + data class Manga( + val libraryManga: LibraryManga, + val isLocal: Boolean = false, + val downloadCount: Long = -1, + val unreadCount: Long = -1, + val language: String = "", + ) : LibraryItem +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 570db5e7ac..c68776b631 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -86,6 +86,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.controller.BaseLegacyController import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.library.compose.LibraryComposeController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.more.AboutController import eu.kanade.tachiyomi.ui.more.OverflowDialog @@ -117,6 +118,7 @@ import eu.kanade.tachiyomi.util.system.prepareSideNavContext import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.blurBehindWindow @@ -136,12 +138,10 @@ import kotlin.collections.set import kotlin.math.abs import kotlin.math.min import kotlin.math.roundToLong -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy import yokai.core.migration.Migrator import yokai.domain.base.BasePreferences @@ -198,6 +198,7 @@ open class MainActivity : BaseActivity() { dimenW to dimenH } + @Deprecated("Create contract directly from Composable") private val requestNotificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (!isGranted) { @@ -538,7 +539,7 @@ open class MainActivity : BaseActivity() { if (currentRoot?.tag()?.toIntOrNull() != id) { setRoot( when (id) { - R.id.nav_library -> LibraryController() + R.id.nav_library -> if (basePreferences.composeLibrary().get()) LibraryComposeController() else LibraryController() R.id.nav_recents -> RecentsController() else -> BrowseController() }, @@ -1001,7 +1002,7 @@ open class MainActivity : BaseActivity() { } private fun checkForAppUpdates() { - if (isUpdaterEnabled) { + if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) { lifecycleScope.launchIO { try { val result = updateChecker.checkForUpdate(this@MainActivity) @@ -1011,7 +1012,7 @@ open class MainActivity : BaseActivity() { val isBeta = result.release.preRelease == true // Create confirmation window - withContext(Dispatchers.Main) { + withUIContext { showNotificationPermissionPrompt() AppUpdateNotifier.releasePageUrl = result.release.releaseLink AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router) @@ -1037,6 +1038,7 @@ open class MainActivity : BaseActivity() { @SuppressLint("MissingSuperCall") override fun onNewIntent(intent: Intent) { + splashState.ready = true if (!handleIntentAction(intent)) { super.onNewIntent(intent) } @@ -1053,13 +1055,13 @@ open class MainActivity : BaseActivity() { } when (intent.action) { SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library - SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, SHORTCUT_RECENTS -> { + SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, Constants.SHORTCUT_RECENTS -> { if (nav.selectedItemId != R.id.nav_recents) { nav.selectedItemId = R.id.nav_recents } else { router.popToRoot() } - if (intent.action == SHORTCUT_RECENTS) return true + if (intent.action == Constants.SHORTCUT_RECENTS) return true nav.post { val controller = router.backstack.firstOrNull()?.controller as? RecentsController @@ -1092,7 +1094,11 @@ open class MainActivity : BaseActivity() { SHORTCUT_UPDATE_NOTES -> { val extras = intent.extras ?: return false if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library - if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) { + if ( + router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController && + // FIXME: Show Compose version of NewUpdateDialog for AboutController + router.backstack.lastOrNull()?.controller !is AboutController + ) { AboutController.NewUpdateDialogController(extras).showDialog(router) } } @@ -1122,7 +1128,6 @@ open class MainActivity : BaseActivity() { else -> return false } - splashState.ready = true return true } @@ -1609,20 +1614,12 @@ open class MainActivity : BaseActivity() { private const val SWIPE_THRESHOLD = 100 private const val SWIPE_VELOCITY_THRESHOLD = 100 - const val MAIN_ACTIVITY = Constants.MAIN_ACTIVITY - // Shortcut actions const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" - @Deprecated("Use the one from Constants object instead") - const val SHORTCUT_RECENTS = Constants.SHORTCUT_RECENTS const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE" const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS" - @Deprecated("Use the one from Constants object instead") - const val SHORTCUT_MANGA = Constants.SHORTCUT_MANGA - @Deprecated("Use the one from Constants object instead") - const val SHORTCUT_MANGA_BACK = Constants.SHORTCUT_MANGA_BACK const val SHORTCUT_UPDATE_NOTES = "eu.kanade.tachiyomi.SHOW_UPDATE_NOTES" const val SHORTCUT_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE" const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt index 894a5468e7..9a01c3873c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt @@ -97,7 +97,7 @@ class SearchActivity : MainActivity() { } private fun intentShouldGoBack() = - intent.action in listOf(SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE) + intent.action in listOf(Constants.SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE) override fun syncActivityViewWithController( to: Controller?, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index be1f17a67e..f4dda11b1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -43,8 +43,8 @@ import uy.kohesive.injekt.injectLazy import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.models.cover import yokai.i18n.MR -import yokai.presentation.core.util.coil.asTarget -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.asTarget +import yokai.util.coil.loadManga import yokai.util.lang.getString import android.R as AR diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 9b288c450c..1337881ee6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -806,6 +806,8 @@ class MangaDetailsController : } private fun getHeader(): MangaHeaderHolder? { + if (!isBindingInitialized) return null + return if (isTablet) { binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index 85e74a2599..9e21f6c4b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -47,7 +47,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.view.resetStrokeColor import yokai.i18n.MR -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga import yokai.util.lang.getString import android.R as AR diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt index 5fcdb7a29b..b8b6e8479e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt @@ -18,6 +18,8 @@ import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SearchOff import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.net.toUri import androidx.core.view.WindowInsetsCompat.Type.systemBars @@ -31,7 +33,6 @@ import com.google.android.material.datepicker.MaterialDatePicker import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.adapters.ItemAdapter import com.mikepenz.fastadapter.listeners.addClickListener -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch @@ -317,7 +318,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) : if (results.isEmpty()) { setMiddleTrackView(binding.searchEmptyView.id) binding.searchEmptyView.show( - R.drawable.ic_search_off_24dp, + Icons.Filled.SearchOff, MR.strings.no_results_found, ) } else { @@ -338,7 +339,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) : binding.trackSearchRecycler.isVisible = false searchItemAdapter.clear() binding.searchEmptyView.show( - R.drawable.ic_search_off_24dp, + Icons.Filled.SearchOff, error.message ?: "", ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt index 6732e191a5..d43c1dfc74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt @@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.view.setCards -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga class MangaHolder( view: View, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt index bb57bc541c..392641714a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt @@ -27,7 +27,7 @@ import yokai.domain.chapter.interactor.GetChapter import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.models.cover import yokai.i18n.MR -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga import yokai.util.lang.getString class MigrationProcessHolder( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt index 5fd462890a..f830d0ff81 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt @@ -1,175 +1,41 @@ package eu.kanade.tachiyomi.ui.more import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Intent +import android.content.Context import android.os.Build import android.os.Bundle +import android.text.Spanned import android.text.method.LinkMovementMethod import android.view.View import android.widget.TextView -import androidx.core.content.getSystemService -import androidx.core.net.toUri -import androidx.preference.PreferenceScreen -import co.touchlab.kermit.Logger -import eu.kanade.tachiyomi.BuildConfig +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.CrossfadeTransition import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob -import eu.kanade.tachiyomi.data.updater.AppUpdateChecker -import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier -import eu.kanade.tachiyomi.data.updater.AppUpdateResult -import eu.kanade.tachiyomi.data.updater.RELEASE_URL +import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController -import eu.kanade.tachiyomi.ui.setting.add -import eu.kanade.tachiyomi.ui.setting.onClick -import eu.kanade.tachiyomi.ui.setting.preference -import eu.kanade.tachiyomi.ui.setting.preferenceCategory -import eu.kanade.tachiyomi.ui.setting.titleMRes -import eu.kanade.tachiyomi.util.CrashLogUtil -import eu.kanade.tachiyomi.util.lang.toTimestampString -import eu.kanade.tachiyomi.util.system.isOnline -import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.materialAlertDialog -import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setNegativeButton import eu.kanade.tachiyomi.util.view.setPositiveButton import eu.kanade.tachiyomi.util.view.setTitle -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.util.view.withFadeTransaction import io.noties.markwon.Markwon -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import yokai.i18n.MR -import yokai.util.lang.getString -import java.text.DateFormat -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.* +import yokai.presentation.settings.screen.about.AboutScreen import android.R as AR -class AboutController : SettingsLegacyController() { +class AboutController : BaseComposeController() { - /** - * Checks for new releases - */ - private val updateChecker by lazy { AppUpdateChecker() } - - private val dateFormat: DateFormat by lazy { - preferences.dateFormat() - } - - private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER - - override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { - titleMRes = MR.strings.about - - preference { - key = "pref_whats_new" - titleMRes = MR.strings.whats_new_this_release - onClick { - val intent = Intent( - Intent.ACTION_VIEW, - if (BuildConfig.DEBUG) { - "https://github.com/null2264/yokai/commits/master" - } else { - RELEASE_URL - }.toUri(), - ) - startActivity(intent) - } - } - if (isUpdaterEnabled) { - preference { - key = "pref_check_for_updates" - titleMRes = MR.strings.check_for_updates - onClick { - if (activity!!.isOnline()) { - checkVersion() - } else { - activity!!.toast(MR.strings.no_network_connection) - } - } - } - } - preference { - key = "pref_version" - titleMRes = MR.strings.version - summary = if (BuildConfig.DEBUG || BuildConfig.NIGHTLY) { - "r" + BuildConfig.COMMIT_COUNT - } else { - BuildConfig.VERSION_NAME - } - - onClick { - activity?.let { - val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo() - val clipboard = it.getSystemService()!! - val appInfo = it.getString(MR.strings.app_info) - clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo)) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - view?.snack(context.getString(MR.strings._copied_to_clipboard, appInfo)) - } - } - } - } - preference { - key = "pref_build_time" - titleMRes = MR.strings.build_time - summary = getFormattedBuildTime(dateFormat) - } - - preferenceCategory { - preference { - key = "pref_oss" - titleMRes = MR.strings.open_source_licenses - - onClick { - router.pushController(AboutLicenseController().withFadeTransaction()) - } - } - } - add(AboutLinksPreference(context)) - } - - /** - * Checks version and shows a user prompt if an update is available. - */ - private fun checkVersion() { - val activity = activity ?: return - - activity.toast(MR.strings.searching_for_updates) - viewScope.launch { - val result = try { - updateChecker.checkForUpdate(activity, true) - } catch (error: Exception) { - withContext(Dispatchers.Main) { - activity.toast(error.message) - Logger.e(error) { "Couldn't check new update" } - } - } - when (result) { - is AppUpdateResult.NewUpdate -> { - val body = result.release.info - val url = result.release.downloadLink - val isBeta = result.release.preRelease == true - - // Create confirmation window - withContext(Dispatchers.Main) { - AppUpdateNotifier.releasePageUrl = result.release.releaseLink - NewUpdateDialogController(body, url, isBeta).showDialog(router) - } - } - is AppUpdateResult.NoNewUpdate -> { - withContext(Dispatchers.Main) { - activity.toast(MR.strings.no_new_updates_available) - } - } - } - } + @Composable + override fun ScreenContent() { + Navigator( + screen = AboutScreen(), + content = { + CrossfadeTransition(navigator = it) + }, + ) } + @Deprecated("Use [DialogHostState.showNewUpdateDialog] instead", ReplaceWith("DialogHostState.showNewUpdateDialog()")) class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { constructor(body: String, url: String, isBeta: Boolean?) : this( @@ -181,9 +47,7 @@ class AboutController : SettingsLegacyController() { ) override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val releaseBody = (args.getString(BODY_KEY) ?: "") - .replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "") - val info = Markwon.create(activity!!).toMarkdown(releaseBody) + val info = activity!!.parseReleaseNotes(args.getString(BODY_KEY) ?: "") val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val isBeta = args.getBoolean(IS_BETA, false) @@ -220,19 +84,9 @@ class AboutController : SettingsLegacyController() { const val IS_BETA = "NewUpdateDialogController.is_beta" } } - - companion object { - fun getFormattedBuildTime(dateFormat: DateFormat): String { - try { - val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault()) - inputDf.timeZone = TimeZone.getTimeZone("UTC") - val buildTime = - inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME - - return buildTime.toTimestampString(dateFormat) - } catch (e: ParseException) { - return BuildConfig.BUILD_TIME - } - } - } +} + +fun Context.parseReleaseNotes(releaseNotes: String): Spanned { + val releaseBody = releaseNotes.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "") + return Markwon.create(this).toMarkdown(releaseBody) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt deleted file mode 100644 index 6a2ea78a8e..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseController.kt +++ /dev/null @@ -1,27 +0,0 @@ -package eu.kanade.tachiyomi.ui.more - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import cafe.adriel.voyager.core.stack.StackEvent -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.transitions.ScreenTransition -import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController -import eu.kanade.tachiyomi.util.compose.LocalBackPress -import soup.compose.material.motion.animation.materialSharedAxisZ - -class AboutLicenseController : BaseComposeController() { - @Composable - override fun ScreenContent() { - Navigator( - screen = AboutLicenseScreen(), - content = { - CompositionLocalProvider(LocalBackPress provides router::handleBack) { - ScreenTransition( - navigator = it, - transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) }, - ) - } - }, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt deleted file mode 100644 index df08c5852d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLinksPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -package eu.kanade.tachiyomi.ui.more - -import android.content.Context -import android.util.AttributeSet -import androidx.preference.Preference -import androidx.preference.PreferenceViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.openInBrowser -import eu.kanade.tachiyomi.util.view.compatToolTipText - -class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - Preference(context, attrs) { - - init { - layoutResource = R.layout.pref_about_links - isSelectable = false - } - - override fun onBindViewHolder(holder: PreferenceViewHolder) { - super.onBindViewHolder(holder) - - /* - (holder.itemView as LinearLayout).apply { - checkHeightThen { - val childCount = (this.getChildAt(0) as ViewGroup).childCount - val childCount2 = (this.getChildAt(1) as ViewGroup).childCount - val fullCount = childCount + childCount2 - orientation = - if (width >= (56 * fullCount).dpToPx) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL - } - } - */ - holder.findViewById(R.id.btn_website).apply { - compatToolTipText = (contentDescription.toString()) - setOnClickListener { context.openInBrowser("https://mihon.app") } - } - holder.findViewById(R.id.btn_discord).apply { - compatToolTipText = (contentDescription.toString()) - setOnClickListener { context.openInBrowser("https://discord.gg/mihon") } - } - holder.findViewById(R.id.btn_github).apply { - compatToolTipText = (contentDescription.toString()) - setOnClickListener { context.openInBrowser("https://github.com/null2264/yokai") } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsController.kt index 054669930b..113df828cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsController.kt @@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.util.system.roundToTwoDecimal import eu.kanade.tachiyomi.util.view.compatToolTipText import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.withFadeTransaction +import kotlin.math.roundToInt import yokai.i18n.MR import yokai.util.lang.getString -import kotlin.math.roundToInt import android.R as AR class StatsController : BaseLegacyController() { @@ -61,7 +61,7 @@ class StatsController : BaseLegacyController() { } private fun handleGeneralStats() { - val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it) } + val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it.manga) } scoresList = getScoresList(mangaTracks) with(binding) { viewDetailLayout.isVisible = mangaDistinct.isNotEmpty() @@ -76,8 +76,8 @@ class StatsController : BaseLegacyController() { } statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString() statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString() - statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString() - statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString() + statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString() + statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString() statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString() statsSourcesText.text = presenter.getSources().count().toString() statsTrackersText.text = presenter.getLoggedTrackers().count().toString() @@ -105,7 +105,7 @@ class StatsController : BaseLegacyController() { val pieEntries = ArrayList() val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) -> - val libraryCount = mangaDistinct.count { it.status == status } + val libraryCount = mangaDistinct.count { it.manga.status == status } if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status))) StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsPresenter.kt index 71cb38068c..6d7c2f3dc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/StatsPresenter.kt @@ -65,19 +65,19 @@ class StatsPresenter( val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt) val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt) val restrictions = prefs.libraryUpdateMangaRestriction().get() - return libraryMangas.groupBy { it.id } + return libraryMangas.groupBy { it.manga.id } .filterNot { it.value.any { manga -> manga.category in excludedCategories } } .filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } } .filterNot { val manga = it.value.first() - (MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED) || + (MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED) || (MANGA_HAS_UNREAD in restrictions && manga.unread != 0) || (MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead) } } fun getDownloadCount(manga: LibraryManga): Int { - return downloadManager.getDownloadCount(manga) + return downloadManager.getDownloadCount(manga.manga) } fun get10PointScore(track: Track): Float? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsController.kt index 7636389cee..c9d366fb39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsController.kt @@ -12,6 +12,8 @@ import android.view.View import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HeartBroken import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.graphics.ColorUtils import androidx.core.util.Pair @@ -458,7 +460,10 @@ class StatsDetailsController : with(binding ?: headerBinding) { val hasNoData = currentStats.isNullOrEmpty() || currentStats.all { it.count == 0 } if (hasNoData) { - this@StatsDetailsController.binding.noChartData.show(R.drawable.ic_heart_off_24dp, MR.strings.no_data_for_filters) + this@StatsDetailsController.binding.noChartData.show( + Icons.Filled.HeartBroken, + MR.strings.no_data_for_filters, + ) presenter.currentStats?.removeAll { it.count == 0 } handleNoChartLayout() this?.statsPieChart?.isVisible = false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt index d47536ffb9..2dc2160d22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/stats/details/StatsDetailsPresenter.kt @@ -153,7 +153,7 @@ class StatsDetailsPresenter( private suspend fun setupSeriesType() { currentStats = ArrayList() - val libraryFormat = mangasDistinct.filterByChip().groupBy { it.seriesType() } + val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.seriesType() } libraryFormat.forEach { (seriesType, mangaList) -> currentStats?.add( @@ -173,7 +173,7 @@ class StatsDetailsPresenter( private suspend fun setupStatus() { currentStats = ArrayList() - val libraryFormat = mangasDistinct.filterByChip().groupBy { it.status } + val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.status } libraryFormat.forEach { (status, mangaList) -> currentStats?.add( @@ -263,7 +263,7 @@ class StatsDetailsPresenter( private suspend fun setupTrackers() { currentStats = ArrayList() val libraryFormat = mangasDistinct.filterByChip() - .map { it to getTracks(it).ifEmpty { listOf(null) } } + .map { it to getTracks(it.manga).ifEmpty { listOf(null) } } .flatMap { it.second.map { track -> it.first to track } } val loggedServices = trackManager.services.filter { it.isLogged } @@ -292,7 +292,7 @@ class StatsDetailsPresenter( private suspend fun setupSources() { currentStats = ArrayList() - val libraryFormat = mangasDistinct.filterByChip().groupBy { it.source } + val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.source } libraryFormat.forEach { (sourceId, mangaList) -> val source = sourceManager.getOrStub(sourceId) @@ -339,10 +339,10 @@ class StatsDetailsPresenter( private suspend fun setupTags() { currentStats = ArrayList() val mangaFiltered = mangasDistinct.filterByChip() - val tags = mangaFiltered.flatMap { it.getTags() }.distinctBy { it.uppercase() } + val tags = mangaFiltered.flatMap { it.manga.getTags() }.distinctBy { it.uppercase() } val libraryFormat = tags.map { tag -> tag to mangaFiltered.filter { - it.getTags().any { mangaTag -> mangaTag.equals(tag, true) } + it.manga.getTags().any { mangaTag -> mangaTag.equals(tag, true) } } } @@ -433,7 +433,7 @@ class StatsDetailsPresenter( this } else { filter { manga -> - context.mapSeriesType(manga.seriesType()) in selectedSeriesType + context.mapSeriesType(manga.manga.seriesType()) in selectedSeriesType } } } @@ -443,7 +443,7 @@ class StatsDetailsPresenter( this } else { filter { manga -> - context.mapStatus(manga.status) in selectedStatus + context.mapStatus(manga.manga.status) in selectedStatus } } } @@ -463,7 +463,7 @@ class StatsDetailsPresenter( this } else { filter { manga -> - manga.source in selectedSource.map { it.id } + manga.manga.source in selectedSource.map { it.id } } } } @@ -504,10 +504,10 @@ class StatsDetailsPresenter( * Get language name of a manga */ private fun LibraryManga.getLanguage(): String { - val code = if (isLocal()) { - LocalSource.getMangaLang(this) + val code = if (manga.isLocal()) { + LocalSource.getMangaLang(this.manga) } else { - sourceManager.get(source)?.lang + sourceManager.get(manga.source)?.lang } ?: return context.getString(MR.strings.unknown) return LocaleHelper.getLocalizedDisplayName(code) } @@ -516,7 +516,7 @@ class StatsDetailsPresenter( * Get mean score rounded to two decimal of a list of manga */ private suspend fun List.getMeanScoreRounded(): Double? { - val mangaTracks = this.map { it to getTracks(it) } + val mangaTracks = this.map { it to getTracks(it.manga) } val scoresList = mangaTracks.filter { it.second.isNotEmpty() } .mapNotNull { it.second.getMeanScoreByTracker() } return if (scoresList.isEmpty()) null else scoresList.average().roundToTwoDecimal() @@ -526,7 +526,7 @@ class StatsDetailsPresenter( * Get mean score rounded to int of a single manga */ private suspend fun LibraryManga.getMeanScoreToInt(): Int? { - val mangaTracks = getTracks(this) + val mangaTracks = getTracks(this.manga) val scoresList = mangaTracks.filter { it.score > 0 } .mapNotNull { it.get10PointScore() } return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10) @@ -550,8 +550,8 @@ class StatsDetailsPresenter( } private suspend fun LibraryManga.getStartYear(): Int? { - if (getChapter.awaitAll(id!!, false).any { it.read }) { - val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 } + if (getChapter.awaitAll(manga.id!!, false).any { it.read }) { + val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 } val date = chapters.minOfOrNull { it.last_read } ?: return null val cal = Calendar.getInstance().apply { timeInMillis = date } return if (date <= 0L) null else cal.get(Calendar.YEAR) @@ -564,7 +564,7 @@ class StatsDetailsPresenter( } private fun getEnabledSources(): List { - return mangasDistinct.mapNotNull { sourceManager.get(it.source) } + return mangasDistinct.mapNotNull { sourceManager.get(it.manga.source) } .distinct().sortedBy { it.name } } @@ -589,7 +589,7 @@ class StatsDetailsPresenter( } private suspend fun List.getReadDuration(): Long { - return sumOf { manga -> getHistory.awaitAllByMangaId(manga.id!!).sumOf { it.time_read } } + return sumOf { manga -> getHistory.awaitAllByMangaId(manga.manga.id!!).sumOf { it.time_read } } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index da7f99e2b8..eb45718063 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -148,6 +148,14 @@ import eu.kanade.tachiyomi.util.view.setMessage import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.widget.doOnEnd import eu.kanade.tachiyomi.widget.doOnStart +import java.io.ByteArrayOutputStream +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Collections +import java.util.Locale +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -167,13 +175,6 @@ import yokai.domain.ui.settings.ReaderPreferences import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour import yokai.i18n.MR import yokai.util.lang.getString -import java.io.ByteArrayOutputStream -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.* -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.roundToInt import android.R as AR /** @@ -512,7 +513,6 @@ class ReaderActivity : BaseActivity() { } } } - viewModel.onSaveInstanceState() super.onSaveInstanceState(outState) } @@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity() { } override fun onPause() { - viewModel.saveCurrentChapterReadingProgress() + viewModel.flushReadTimer() super.onPause() } override fun onResume() { super.onResume() - viewModel.setReadStartTime() + viewModel.restartReadTimer() } fun reloadChapters(doublePages: Boolean, force: Boolean = false) { @@ -1655,7 +1655,7 @@ class ReaderActivity : BaseActivity() { } private fun showSetCoverPrompt(page: ReaderPage) { - if (page.status != Page.State.READY) return + if (page.status !is Page.State.Ready) return materialAlertDialog() .setMessage(MR.strings.use_image_as_cover) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index cf0a856541..5097eb1809 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -12,6 +12,7 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.create import eu.kanade.tachiyomi.data.database.models.defaultReaderType import eu.kanade.tachiyomi.data.database.models.orientationType import eu.kanade.tachiyomi.data.database.models.readingModeType @@ -54,9 +55,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withUIContext import java.util.Date import java.util.concurrent.CancellationException -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -68,6 +67,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -80,6 +80,7 @@ import yokai.domain.chapter.models.ChapterUpdate import yokai.domain.download.DownloadPreferences import yokai.domain.history.interactor.GetHistory import yokai.domain.history.interactor.UpsertHistory +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.UpdateManga @@ -101,6 +102,7 @@ class ReaderViewModel( private val chapterFilter: ChapterFilter = Injekt.get(), private val storageManager: StorageManager = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), ) : ViewModel() { private val getCategories: GetCategories by injectLazy() private val getChapter: GetChapter by injectLazy() @@ -155,12 +157,15 @@ class ReaderViewModel( private var finished = false private var chapterToDownload: Download? = null + private val unfilteredChapterList by lazy { + val manga = manga!! + runBlocking { getChapter.awaitAll(manga, filterScanlators = false) } + } + private lateinit var chapterList: List private var chapterItems = emptyList() - private var scope = CoroutineScope(Job() + Dispatchers.Default) - private var hasTrackers: Boolean = false private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty() @@ -191,24 +196,12 @@ class ReaderViewModel( val currentChapters = state.value.viewerChapters if (currentChapters != null) { currentChapters.unref() - saveReadingProgress(currentChapters.currChapter) chapterToDownload?.let { downloadManager.addDownloadsToStartOfQueue(listOf(it)) } } } - /** - * Called when the activity is saved and not changing configurations. It updates the database - * to persist the current progress of the active chapter. - */ - fun onSaveInstanceState() { - val currentChapter = getCurrentChapter() ?: return - viewModelScope.launchNonCancellableIO { - saveChapterProgress(currentChapter) - } - } - /** * Whether this presenter is initialized yet. */ @@ -307,6 +300,7 @@ class ReaderViewModel( return delegatedSource.pageNumber(url)?.minus(1) } + // FIXME: Unused at the moment, handles J2K's delegated deep link, refactor or remove later suspend fun loadChapterURL(url: Uri) { val host = url.host ?: return val context = Injekt.get() @@ -314,9 +308,7 @@ class ReaderViewModel( context.getString(MR.strings.source_not_installed), ) val chapterUrl = delegatedSource.chapterUrl(url) - val sourceId = delegatedSource.delegate?.id ?: error( - context.getString(MR.strings.source_not_installed), - ) + val sourceId = delegatedSource.delegate.id if (chapterUrl != null) { val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find { val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false @@ -334,7 +326,9 @@ class ReaderViewModel( } val info = delegatedSource.fetchMangaFromChapterUrl(url) if (info != null) { - val (chapter, manga, chapters) = info + val (sChapter, sManga, chapters) = info + val manga = Manga.create(sManga.url, sManga.title, sourceId).apply { copyFrom(sManga) } + val chapter = Chapter.create().apply { copyFrom(sChapter) } val id = insertManga.await(manga) manga.id = id ?: manga.id chapter.manga_id = manga.id @@ -373,12 +367,15 @@ class ReaderViewModel( * Called when the user changed to the given [chapter] when changing pages from the viewer. * It's used only to set this chapter as active. */ - private suspend fun loadNewChapter(chapter: ReaderChapter) { + private fun loadNewChapter(chapter: ReaderChapter) { val loader = loader ?: return - Logger.d { "Loading ${chapter.chapter.url}" } + viewModelScope.launchIO { + Logger.d { "Loading ${chapter.chapter.url}" } + + flushReadTimer() + restartReadTimer() - withIOContext { try { loadChapter(loader, chapter) } catch (e: Throwable) { @@ -509,28 +506,15 @@ class ReaderViewModel( val selectedChapter = page.chapter // Save last page read and mark as read if needed - selectedChapter.chapter.last_page_read = page.index - selectedChapter.chapter.pages_left = - (selectedChapter.pages?.size ?: page.index) - page.index - val shouldTrack = !preferences.incognitoMode().get() || hasTrackers - if (shouldTrack && - // For double pages, check if the second to last page is doubled up - ( - (selectedChapter.pages?.lastIndex == page.index && page.firstHalf != true) || - (hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index) - ) - ) { - selectedChapter.chapter.read = true - updateTrackChapterAfterReading(selectedChapter) - deleteChapterIfNeeded(selectedChapter) + viewModelScope.launchNonCancellableIO { + saveChapterProgress(selectedChapter, page, hasExtraPage) } if (selectedChapter != currentChapters.currChapter) { Logger.d { "Setting ${selectedChapter.chapter.url} as active" } - saveReadingProgress(currentChapters.currChapter) - setReadStartTime() - scope.launch { loadNewChapter(selectedChapter) } + loadNewChapter(selectedChapter) } + val pages = page.chapter.pages ?: return val inDownloadRange = page.number.toDouble() / pages.size > 0.2 if (inDownloadRange) { @@ -618,28 +602,28 @@ class ReaderViewModel( } } - /** - * Called when reader chapter is changed in reader or when activity is paused. - */ - private fun saveReadingProgress(readerChapter: ReaderChapter) { - viewModelScope.launchNonCancellableIO { - saveChapterProgress(readerChapter) - saveChapterHistory(readerChapter) - } - } - - fun saveCurrentChapterReadingProgress() = getCurrentChapter()?.let { saveReadingProgress(it) } - /** * Saves this [readerChapter]'s progress (last read page and whether it's read). * If incognito mode isn't on or has at least 1 tracker */ - private suspend fun saveChapterProgress(readerChapter: ReaderChapter) { + private suspend fun saveChapterProgress(readerChapter: ReaderChapter, page: ReaderPage, hasExtraPage: Boolean) { readerChapter.requestedPage = readerChapter.chapter.last_page_read getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter -> readerChapter.chapter.bookmark = dbChapter.bookmark } - if (!preferences.incognitoMode().get() || hasTrackers) { + + val shouldTrack = !preferences.incognitoMode().get() || hasTrackers + if (shouldTrack && page.status !is Page.State.Error) { + readerChapter.chapter.last_page_read = page.index + readerChapter.chapter.pages_left = (readerChapter.pages?.size ?: page.index) - page.index + // For double pages, check if the second to last page is doubled up + if ( + (readerChapter.pages?.lastIndex == page.index && page.firstHalf != true) || + (hasExtraPage && readerChapter.pages?.lastIndex?.minus(1) == page.index) + ) { + onChapterReadComplete(readerChapter) + } + updateChapter.await( ChapterUpdate( id = readerChapter.chapter.id!!, @@ -652,24 +636,56 @@ class ReaderViewModel( } } + private suspend fun onChapterReadComplete(readerChapter: ReaderChapter) { + readerChapter.chapter.read = true + updateTrackChapterAfterReading(readerChapter) + deleteChapterIfNeeded(readerChapter) + + val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() + .contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING) + if (!markDuplicateAsRead) return + + val duplicateUnreadChapters = unfilteredChapterList + .mapNotNull { chapter -> + if ( + !chapter.read && + chapter.isRecognizedNumber && + chapter.chapter_number == readerChapter.chapter.chapter_number + ) { + ChapterUpdate(id = chapter.id!!, read = true) + } else { + null + } + } + updateChapter.awaitAll(duplicateUnreadChapters) + } + + fun restartReadTimer() { + chapterReadStartTime = Date().time + } + + fun flushReadTimer() { + getCurrentChapter()?.let { + viewModelScope.launchNonCancellableIO { + saveChapterHistory(it) + } + } + } + /** * Saves this [readerChapter] last read history. */ private suspend fun saveChapterHistory(readerChapter: ReaderChapter) { - if (!preferences.incognitoMode().get()) { - val readAt = Date().time - val sessionReadDuration = chapterReadStartTime?.let { readAt - it } ?: 0 - val history = History.create(readerChapter.chapter).apply { - last_read = readAt - time_read = sessionReadDuration - } - upsertHistory.await(history) - chapterReadStartTime = null - } - } + if (preferences.incognitoMode().get()) return - fun setReadStartTime() { - chapterReadStartTime = Date().time + val endTime = Date().time + val sessionReadDuration = chapterReadStartTime?.let { endTime - it } ?: 0 + val history = History.create(readerChapter.chapter).apply { + last_read = endTime + time_read = sessionReadDuration + } + upsertHistory.await(history) + chapterReadStartTime = null } /** @@ -844,7 +860,7 @@ class ReaderViewModel( * There's also a notification to allow sharing the image somewhere else or deleting it. */ fun saveImage(page: ReaderPage) { - if (page.status != Page.State.READY) return + if (page.status !is Page.State.Ready) return val manga = manga ?: return val context = Injekt.get() @@ -874,9 +890,9 @@ class ReaderViewModel( } fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - scope.launch { - if (firstPage.status != Page.State.READY) return@launch - if (secondPage.status != Page.State.READY) return@launch + viewModelScope.launch { + if (firstPage.status !is Page.State.Ready) return@launch + if (secondPage.status !is Page.State.Ready) return@launch val manga = manga ?: return@launch val context = Injekt.get() @@ -910,7 +926,7 @@ class ReaderViewModel( * image will be kept so it won't be taking lots of internal disk space. */ fun shareImage(page: ReaderPage) { - if (page.status != Page.State.READY) return + if (page.status !is Page.State.Ready) return val manga = manga ?: return val context = Injekt.get() @@ -923,9 +939,9 @@ class ReaderViewModel( } fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - scope.launch { - if (firstPage.status != Page.State.READY) return@launch - if (secondPage.status != Page.State.READY) return@launch + viewModelScope.launch { + if (firstPage.status !is Page.State.Ready) return@launch + if (secondPage.status !is Page.State.Ready) return@launch val manga = manga ?: return@launch val context = Injekt.get() @@ -942,7 +958,7 @@ class ReaderViewModel( * Sets the image of this [page] as cover and notifies the UI of the result. */ fun setAsCover(page: ReaderPage) { - if (page.status != Page.State.READY) return + if (page.status !is Page.State.Ready) return val manga = manga ?: return val stream = page.stream ?: return diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index ad78920502..16c5a371fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt @@ -11,6 +11,8 @@ import yokai.core.archive.ArchiveReader */ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() { + override val isLocal: Boolean = true + /** * Recycles this loader and the open archive. */ @@ -29,7 +31,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader .mapIndexed { i, entry -> ReaderPage(i).apply { stream = { reader.getInputStream(entry.name)!! } - status = Page.State.READY + status = Page.State.Ready } } .toList() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 7756cdd585..44f89294e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -10,7 +10,8 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.util.system.withIOContext -import yokai.core.archive.archiveReader +import yokai.core.archive.util.archiveReader +import yokai.core.archive.util.epubReader import yokai.i18n.MR import yokai.util.lang.getString @@ -82,7 +83,7 @@ class ChapterLoader( when (format) { is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) is LocalSource.Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) - is LocalSource.Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) + is LocalSource.Format.Epub -> EpubPageLoader(format.file.epubReader(context)) } } else -> error(context.getString(MR.strings.source_not_installed)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index e6557c5a13..825b14a9dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.util.system.ImageUtil */ class DirectoryPageLoader(val file: UniFile) : PageLoader() { + override val isLocal: Boolean = true + /** * Returns the pages found on this directory ordered with a natural comparator. */ @@ -22,7 +24,7 @@ class DirectoryPageLoader(val file: UniFile) : PageLoader() { val streamFn = { file.openInputStream() } ReaderPage(i).apply { stream = streamFn - status = Page.State.READY + status = Page.State.Ready } } ?: emptyList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index f2c2ea8a46..b6efbb1fcf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import uy.kohesive.injekt.injectLazy -import yokai.core.archive.archiveReader +import yokai.core.archive.util.archiveReader /** * Loader used to load a chapter from the downloaded chapters. @@ -24,6 +24,8 @@ class DownloadPageLoader( private val downloadProvider: DownloadProvider, ) : PageLoader() { + override val isLocal: Boolean = true + // Needed to open input streams private val context: Application by injectLazy() @@ -58,7 +60,7 @@ class DownloadPageLoader( ReaderPage(page.index, page.url, page.imageUrl, stream = { context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!! },).apply { - status = Page.State.READY + status = Page.State.Ready } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 53e0f26e74..c5a713fa83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -2,18 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.storage.EpubFile -import yokai.core.archive.ArchiveReader +import yokai.core.archive.EpubReader /** * Loader used to load a chapter from a .epub file. */ -class EpubPageLoader(reader: ArchiveReader) : PageLoader() { +class EpubPageLoader(private val epub: EpubReader) : PageLoader() { - /** - * The epub file. - */ - private val epub = EpubFile(reader) + override val isLocal: Boolean = true /** * Recycles this loader and the open zip. @@ -32,7 +28,7 @@ class EpubPageLoader(reader: ArchiveReader) : PageLoader() { val streamFn = { epub.getInputStream(path)!! } ReaderPage(i).apply { stream = streamFn - status = Page.State.READY + status = Page.State.Ready } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 130a7fee57..4c91b5ea8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -8,6 +8,9 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.withIOContext +import java.util.concurrent.PriorityBlockingQueue +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.min import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,9 +22,6 @@ import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.suspendCancellableCoroutine import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.concurrent.* -import java.util.concurrent.atomic.* -import kotlin.math.min /** * Loader used to load chapters from an online source. @@ -33,6 +33,8 @@ class HttpPageLoader( private val preferences: PreferencesHelper = Injekt.get(), ) : PageLoader() { + override val isLocal: Boolean = false + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) /** @@ -49,7 +51,7 @@ class HttpPageLoader( emit(runInterruptible { queue.take() }.page) } } - .filter { it.status == Page.State.QUEUE } + .filter { it.status is Page.State.Queue } .collect { _loadPage(it) } @@ -106,17 +108,17 @@ class HttpPageLoader( val imageUrl = page.imageUrl // Check if the image has been deleted - if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) { - page.status = Page.State.QUEUE + if (page.status is Page.State.Ready && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) { + page.status = Page.State.Queue } // Automatically retry failed pages when subscribed to this page - if (page.status == Page.State.ERROR) { - page.status = Page.State.QUEUE + if (page.status is Page.State.Error) { + page.status = Page.State.Queue } val queuedPages = mutableListOf() - if (page.status == Page.State.QUEUE) { + if (page.status is Page.State.Queue) { queuedPages += PriorityPage(page, 1).also { queue.offer(it) } } queuedPages += preloadNextPages(page, preloadSize) @@ -124,7 +126,7 @@ class HttpPageLoader( suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { queuedPages.forEach { - if (it.page.status == Page.State.QUEUE) { + if (it.page.status is Page.State.Queue) { queue.remove(it) } } @@ -144,7 +146,7 @@ class HttpPageLoader( return pages .subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size)) .mapNotNull { - if (it.status == Page.State.QUEUE) { + if (it.status is Page.State.Queue) { PriorityPage(it, 0).apply { queue.offer(this) } } else { null @@ -156,8 +158,8 @@ class HttpPageLoader( * Retries a page. This method is only called from user interaction on the viewer. */ override fun retryPage(page: ReaderPage) { - if (page.status == Page.State.ERROR) { - page.status = Page.State.QUEUE + if (page.status is Page.State.Error) { + page.status = Page.State.Queue } queue.offer(PriorityPage(page, 2)) } @@ -190,21 +192,21 @@ class HttpPageLoader( private suspend fun _loadPage(page: ReaderPage) { try { if (page.imageUrl.isNullOrEmpty()) { - page.status = Page.State.LOAD_PAGE + page.status = Page.State.LoadPage page.imageUrl = source.getImageUrl(page) } val imageUrl = page.imageUrl!! if (!chapterCache.isImageInCache(imageUrl)) { - page.status = Page.State.DOWNLOAD_IMAGE + page.status = Page.State.DownloadImage val imageResponse = source.getImage(page) chapterCache.putImageToCache(imageUrl, imageResponse) } page.stream = { chapterCache.getImageFile(imageUrl).inputStream() } - page.status = Page.State.READY + page.status = Page.State.Ready } catch (e: Throwable) { - page.status = Page.State.ERROR + page.status = Page.State.Error if (e is CancellationException) { throw e } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt index 720e81a43c..3362ff3caa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage */ abstract class PageLoader { + abstract val isLocal: Boolean + /** * Whether this loader has been already recycled. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt index 254a7d6309..5bb4ec428d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/InsertPage.kt @@ -12,6 +12,6 @@ class InsertPage(parent: ReaderPage) : ReaderPage( fullPage = true firstHalf = false stream = parent.stream - status = State.READY + status = State.Ready } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 3693274498..109d260dd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -1,155 +1,100 @@ package eu.kanade.tachiyomi.ui.reader.viewer import android.content.Context -import android.text.SpannableStringBuilder -import android.text.style.ImageSpan import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.LinearLayout -import androidx.annotation.ColorInt -import androidx.core.text.bold -import androidx.core.text.buildSpannedString -import androidx.core.text.inSpans -import androidx.core.view.isVisible -import eu.kanade.tachiyomi.R +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.AbstractComposeView import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition -import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName -import eu.kanade.tachiyomi.util.system.contextCompatDrawable -import eu.kanade.tachiyomi.util.system.dpToPx -import uy.kohesive.injekt.injectLazy -import yokai.i18n.MR -import yokai.util.lang.getString -import kotlin.math.roundToInt +import eu.kanade.tachiyomi.util.isLocal +import eu.kanade.tachiyomi.util.system.ThemeUtil +import yokai.presentation.reader.ChapterTransition +import yokai.presentation.theme.YokaiTheme class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - LinearLayout(context, attrs) { + AbstractComposeView(context, attrs) { - private val binding: ReaderTransitionViewBinding = - ReaderTransitionViewBinding.inflate(LayoutInflater.from(context), this, true) - private val preferences: PreferencesHelper by injectLazy() + private var data: Data? by mutableStateOf(null) init { layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } - fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { - manga ?: return - when (transition) { - is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga) - is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga) - } - - missingChapterWarning(transition) - } - - /** - * Binds a previous chapter transition on this view and subscribes to the page load status. - */ - private fun bindPrevChapterTransition( - transition: ChapterTransition, - downloadManager: DownloadManager, - manga: Manga, - ) { - val prevChapter = transition.to - - binding.lowerText.isVisible = prevChapter != null - if (prevChapter != null) { - binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START - val isPrevDownloaded = downloadManager.isChapterDownloaded(prevChapter.chapter, manga) - val isCurrentDownloaded = downloadManager.isChapterDownloaded(transition.from.chapter, manga) - binding.upperText.text = buildSpannedString { - bold { append(context.getString(MR.strings.previous_title)) } - append("\n${prevChapter.chapter.preferredChapterName(context, manga, preferences)}") - if (isPrevDownloaded != isCurrentDownloaded) addDLImageSpan(isPrevDownloaded) - } - binding.lowerText.text = buildSpannedString { - bold { append(context.getString(MR.strings.current_chapter)) } - val name = transition.from.chapter.preferredChapterName(context, manga, preferences) - append("\n$name") - } + fun bind(theme: Int, transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { + data = if (manga != null) { + Data( + theme = theme, + manga = manga, + transition = transition, + currChapterDownloaded = transition.from.pageLoader?.isLocal == true, + goingToChapterDownloaded = manga.isLocal() || + transition.to?.chapter?.let { goingToChapter -> + downloadManager.isChapterDownloaded( + chapter = goingToChapter, + manga = manga, + skipCache = true, + ) + } ?: false, + ) } else { - binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER - binding.upperText.text = context.getString(MR.strings.theres_no_previous_chapter) + null } } - /** - * Binds a next chapter transition on this view and subscribes to the load status. - */ - private fun bindNextChapterTransition( - transition: ChapterTransition, - downloadManager: DownloadManager, - manga: Manga, - ) { - val nextChapter = transition.to - - binding.lowerText.isVisible = nextChapter != null - if (nextChapter != null) { - binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START - val isCurrentDownloaded = downloadManager.isChapterDownloaded(transition.from.chapter, manga) - val isNextDownloaded = downloadManager.isChapterDownloaded(nextChapter.chapter, manga) - binding.upperText.text = buildSpannedString { - bold { append(context.getString(MR.strings.finished_chapter)) } - val name = transition.from.chapter.preferredChapterName(context, manga, preferences) - append("\n$name") + @Composable + override fun Content() { + data?.let { + YokaiTheme { + CompositionLocalProvider ( + LocalTextStyle provides MaterialTheme.typography.bodySmall, + LocalContentColor provides ThemeUtil.readerContentColor(it.theme, MaterialTheme.colorScheme.onBackground), + ) { + ChapterTransition( + manga = it.manga, + transition = it.transition, + currChapterDownloaded = it.currChapterDownloaded, + goingToChapterDownloaded = it.goingToChapterDownloaded, + ) + } } - binding.lowerText.text = buildSpannedString { - bold { append(context.getString(MR.strings.next_title)) } - append("\n${nextChapter.chapter.preferredChapterName(context, manga, preferences)}") - if (isNextDownloaded != isCurrentDownloaded) addDLImageSpan(isNextDownloaded) - } - } else { - binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER - binding.upperText.text = context.getString(MR.strings.theres_no_next_chapter) } } - private fun SpannableStringBuilder.addDLImageSpan(isDownloaded: Boolean) { - val icon = context.contextCompatDrawable( - if (isDownloaded) R.drawable.ic_file_download_24dp else R.drawable.ic_cloud_24dp, - ) - ?.mutate() - ?.apply { - val size = binding.lowerText.textSize + 4f.dpToPx - setTint(binding.lowerText.currentTextColor) - setBounds(0, 0, size.roundToInt(), size.roundToInt()) - } ?: return - append(" ") - inSpans(ImageSpan(icon)) { append("image") } - } - - fun setTextColors(@ColorInt color: Int) { - binding.upperText.setTextColor(color) - binding.warningText.setTextColor(color) - binding.lowerText.setTextColor(color) - } - - private fun missingChapterWarning(transition: ChapterTransition) { - if (transition.to == null) { - binding.warning.isVisible = false - return - } - - val hasMissingChapters = when (transition) { - is ChapterTransition.Prev -> hasMissingChapters(transition.from, transition.to) - is ChapterTransition.Next -> hasMissingChapters(transition.to, transition.from) - } - - if (!hasMissingChapters) { - binding.warning.isVisible = false - return - } - - val chapterDifference = when (transition) { - is ChapterTransition.Prev -> calculateChapterDifference(transition.from, transition.to) - is ChapterTransition.Next -> calculateChapterDifference(transition.to, transition.from) - } - - binding.warningText.text = context.getString(MR.plurals.missing_chapters_warning, chapterDifference.toInt(), chapterDifference.toInt()) - binding.warning.isVisible = true - } + private data class Data( + val theme: Int, + val manga: Manga, + val transition: ChapterTransition, + val currChapterDownloaded: Boolean, + val goingToChapterDownloaded: Boolean, + ) +} + +fun missingChapterCount(transition: ChapterTransition): Int { + if (transition.to == null) { + return 0 + } + + val hasMissingChapters = when (transition) { + is ChapterTransition.Prev -> hasMissingChapters(transition.from, transition.to) + is ChapterTransition.Next -> hasMissingChapters(transition.to, transition.from) + } + + if (!hasMissingChapters) { + return 0 + } + + val chapterDifference = when (transition) { + is ChapterTransition.Prev -> calculateChapterDifference(transition.from, transition.to) + is ChapterTransition.Next -> calculateChapterDifference(transition.to, transition.from) + } + + return chapterDifference.toInt() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 8dc4dcc000..96d657296a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -109,8 +109,8 @@ class PagerPageHolder( */ private var extraProgressJob: Job? = null - private var status = Page.State.READY - private var extraStatus = Page.State.READY + private var status = Page.State.Ready + private var extraStatus = Page.State.Ready private var progress: Int = 0 private var extraProgress: Int = 0 @@ -337,19 +337,19 @@ class PagerPageHolder( */ private suspend fun processStatus(status: Page.State) { when (status) { - Page.State.QUEUE -> setQueued() - Page.State.LOAD_PAGE -> setLoading() - Page.State.DOWNLOAD_IMAGE -> { + is Page.State.Queue -> setQueued() + is Page.State.LoadPage -> setLoading() + is Page.State.DownloadImage -> { launchProgressJob() setDownloading() } - Page.State.READY -> { - if (extraStatus == Page.State.READY || extraPage == null) { + is Page.State.Ready -> { + if (extraPage == null) { setImage() } cancelProgressJob(1) } - Page.State.ERROR -> { + is Page.State.Error -> { setError() cancelProgressJob(1) } @@ -363,19 +363,17 @@ class PagerPageHolder( */ private suspend fun processStatus2(status: Page.State) { when (status) { - Page.State.QUEUE -> setQueued() - Page.State.LOAD_PAGE -> setLoading() - Page.State.DOWNLOAD_IMAGE -> { + is Page.State.Queue -> setQueued() + is Page.State.LoadPage -> setLoading() + is Page.State.DownloadImage -> { launchProgressJob2() setDownloading() } - Page.State.READY -> { - if (this.status == Page.State.READY) { - setImage() - } + is Page.State.Ready -> { + setImage() cancelProgressJob(2) } - Page.State.ERROR -> { + is Page.State.Error -> { setError() cancelProgressJob(2) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index c2854b2c0d..7114bbc499 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -14,14 +14,11 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.updatePaddingRelative -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.viewer.ReaderButton import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView -import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.dpToPx -import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setText import eu.kanade.tachiyomi.widget.ViewPagerAdapter import kotlinx.coroutines.Job @@ -67,17 +64,11 @@ class PagerTransitionHolder( setPadding(sidePadding, 0, sidePadding, 0) val transitionView = ReaderTransitionView(context) - transitionView.setTextColors( - ThemeUtil.readerContentColor( - viewer.config.readerTheme, - context.getResourceColor(R.attr.colorOnBackground), - ) - ) addView(transitionView) addView(pagesContainer) - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transitionView.bind(viewer.config.readerTheme, transition, viewer.downloadManager, viewer.activity.viewModel.manga) transition.to?.let { observeStatus(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index e1428aa4cc..52c60f7adc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -128,9 +128,9 @@ class WebtoonPageHolder( launchIO { loader.loadPage(page) } page.statusFlow.collectLatest { status -> when (status) { - Page.State.QUEUE -> setQueued() - Page.State.LOAD_PAGE -> setLoading() - Page.State.DOWNLOAD_IMAGE -> { + is Page.State.Queue -> setQueued() + is Page.State.LoadPage -> setLoading() + is Page.State.DownloadImage -> { setDownloading() scope.launch { page.progressFlow.collectLatest { value -> @@ -138,8 +138,8 @@ class WebtoonPageHolder( } } } - Page.State.READY -> setImage() - Page.State.ERROR -> setError() + is Page.State.Ready -> setImage() + is Page.State.Error -> setError() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 1fb8d78df2..cfce248456 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -12,13 +12,10 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isNotEmpty import androidx.core.view.isVisible -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView -import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.dpToPx -import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setText import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope @@ -72,13 +69,7 @@ class WebtoonTransitionHolder( * Binds the given [transition] with this view holder, subscribing to its state. */ fun bind(transition: ChapterTransition) { - transitionView.setTextColors( - ThemeUtil.readerContentColor( - viewer.config.readerTheme, - context.getResourceColor(R.attr.colorOnBackground), - ) - ) - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transitionView.bind(viewer.config.readerTheme, transition, viewer.downloadManager, viewer.activity.viewModel.manga) transition.to?.let { observeStatus(it, transition) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt index 24e003c2dc..dadc03fb3a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt @@ -34,7 +34,7 @@ import eu.kanade.tachiyomi.util.view.setCards import java.util.Date import java.util.concurrent.TimeUnit import yokai.i18n.MR -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga import yokai.util.lang.getString import android.R as AR diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 022d491fd4..b4ec025691 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -12,6 +12,9 @@ import android.view.RoundedCorner import android.view.View import android.view.ViewGroup import androidx.activity.BackEventCompat +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HistoryToggleOff +import androidx.compose.material.icons.filled.SearchOff import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -596,9 +599,9 @@ class RecentsController(bundle: Bundle? = null) : if (recents.isEmpty()) { binding.recentsEmptyView.show( if (!isSearching()) { - R.drawable.ic_history_off_24dp + Icons.Filled.HistoryToggleOff } else { - R.drawable.ic_search_off_24dp + Icons.Filled.SearchOff }, if (isSearching()) { MR.strings.no_results_found diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 138df111a7..b29671a580 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -75,8 +75,8 @@ class RecentsPresenter( } private val newAdditionsHeader = RecentMangaHeaderItem(RecentMangaHeaderItem.NEWLY_ADDED) private val newChaptersHeader = RecentMangaHeaderItem(RecentMangaHeaderItem.NEW_CHAPTERS) - private val continueReadingHeader = - RecentMangaHeaderItem(RecentMangaHeaderItem.CONTINUE_READING) + private val continueReadingHeader = RecentMangaHeaderItem(RecentMangaHeaderItem.CONTINUE_READING) + var finished = false private var shouldMoveToTop = false var viewType: RecentsViewType = RecentsViewType.valueOf(uiPreferences.recentsViewType().get()) @@ -113,9 +113,9 @@ class RecentsPresenter( } presenterScope.launchIO { downloadManager.queueState.collectLatest { - setDownloadedChapters(recentItems, it) + if (recentItems.isNotEmpty()) setDownloadedChapters(recentItems, it) withUIContext { - view?.showLists(recentItems, true) + if (recentItems.isNotEmpty()) view?.showLists(recentItems, true) view?.updateDownloadStatus(!downloadManager.isPaused()) } } @@ -379,20 +379,22 @@ class RecentsPresenter( f2.second.date_fetch.compareTo(f1.second.date_fetch) } } - .take(4).map { - RecentMangaItem(it.first, it.second, newChaptersHeader) - }.toMutableList() + .take(UPDATES_CHAPTER_LIMIT) + .map { RecentMangaItem(it.first, it.second, newChaptersHeader) } + .toMutableList() val cReadingItems = - pairs.filter { it.first.history.id != null }.take(9 - nChaptersItems.size).map { - RecentMangaItem(it.first, it.second, continueReadingHeader) - }.toMutableList() + pairs.filter { it.first.history.id != null } + .take(UPDATES_READING_LIMIT_UPPER - nChaptersItems.size) + .map { RecentMangaItem(it.first, it.second, continueReadingHeader) } + .toMutableList() if (nChaptersItems.isNotEmpty()) { nChaptersItems.add(RecentMangaItem(header = newChaptersHeader)) } if (cReadingItems.isNotEmpty()) { cReadingItems.add(RecentMangaItem(header = continueReadingHeader)) } - val nAdditionsItems = pairs.filter { it.first.chapter.id == null }.take(4) + val nAdditionsItems = pairs.filter { it.first.chapter.id == null } + .take(UPDATES_CHAPTER_LIMIT) .map { RecentMangaItem(it.first, it.second, newAdditionsHeader) } listOf(nChaptersItems, cReadingItems, nAdditionsItems).sortedByDescending { it.firstOrNull()?.mch?.history?.last_read ?: 0L @@ -483,20 +485,6 @@ class RecentsPresenter( return Triple(sortedChapters, firstChapter, extraCount) } - private suspend fun getNextChapter(manga: Manga): Chapter? { - val mangaId = manga.id ?: return null - val chapters = getChapter.awaitUnread(mangaId, true) - return ChapterSort(manga, chapterFilter, preferences).getNextChapter(chapters, false) - } - - private suspend fun getFirstUpdatedChapter(manga: Manga, chapter: Chapter): Chapter? { - val mangaId = manga.id ?: return null - val chapters = getChapter.awaitUnread(mangaId, true) - return chapters.sortedWith(ChapterSort(manga, chapterFilter, preferences).sortComparator(true)).find { - abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) - } - } - override fun onDestroy() { super.onDestroy() lastRecents = recentItems @@ -727,6 +715,31 @@ class RecentsPresenter( var SHORT_LIMIT = 25 private set + suspend fun getNextChapter( + manga: Manga, + getChapter: GetChapter = Injekt.get(), + chapterFilter: ChapterFilter = Injekt.get(), + preferences: PreferencesHelper = Injekt.get(), + ): Chapter? { + val mangaId = manga.id ?: return null + val chapters = getChapter.awaitUnread(mangaId, true) + return ChapterSort(manga, chapterFilter, preferences).getNextChapter(chapters, false) + } + + suspend fun getFirstUpdatedChapter( + manga: Manga, + chapter: Chapter, + getChapter: GetChapter = Injekt.get(), + chapterFilter: ChapterFilter = Injekt.get(), + preferences: PreferencesHelper = Injekt.get(), + ): Chapter? { + val mangaId = manga.id ?: return null + val chapters = getChapter.awaitUnread(mangaId, true) + return chapters.sortedWith(ChapterSort(manga, chapterFilter, preferences).sortComparator(true)).find { + abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) + } + } + suspend fun getRecentManga(includeRead: Boolean = false, customAmount: Int = 0): List> { val presenter = RecentsPresenter() presenter.viewType = RecentsViewType.UngroupedAll @@ -741,5 +754,9 @@ class RecentsPresenter( .filter { it.mch.manga.id != null } .map { it.mch.manga to it.mch.history.last_read } } + + const val UPDATES_CHAPTER_LIMIT = 4 + const val UPDATES_READING_LIMIT_UPPER = 9 + const val UPDATES_READING_LIMIT_LOWER = 5 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt index 9912f988fe..0aeed45248 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt @@ -1,11 +1,7 @@ package eu.kanade.tachiyomi.ui.setting import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController -import eu.kanade.tachiyomi.util.compose.LocalAlertDialog -import eu.kanade.tachiyomi.util.compose.LocalBackPress -import yokai.domain.ComposableAlertDialog import yokai.presentation.settings.ComposableSettings abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface { @@ -18,11 +14,6 @@ abstract class SettingsComposeController: BaseComposeController(), SettingsContr @Composable override fun ScreenContent() { - CompositionLocalProvider( - LocalAlertDialog provides ComposableAlertDialog(null), - LocalBackPress provides router::handleBack, - ) { - getComposableSettings().Content() - } + getComposableSettings().Content() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt index 6f2616dd61..8724eea63c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt @@ -24,7 +24,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob.Target import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.changesIn -import eu.kanade.tachiyomi.extension.ShizukuInstaller +import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.PREF_DOH_360 @@ -394,6 +394,12 @@ class SettingsAdvancedController : SettingsLegacyController() { onClick { LibraryUpdateJob.startNow(context, target = Target.TRACKING) } } + if (BuildConfig.FLAVOR == "dev" || BuildConfig.DEBUG) { + switchPreference { + bindTo(basePreferences.composeLibrary()) + title = context.getString(MR.strings.pref_use_compose_library).addBetaTag(context) + } + } } preferenceCategory { @@ -453,36 +459,37 @@ class SettingsAdvancedController : SettingsLegacyController() { } } - preferenceCategory { - title = "Danger zone!" - isVisible = BuildConfig.FLAVOR == "dev" || BuildConfig.DEBUG || BuildConfig.NIGHTLY + if (BuildConfig.FLAVOR == "dev" || BuildConfig.DEBUG || BuildConfig.NIGHTLY) { + preferenceCategory { + title = "Danger zone!" - preference { - title = "Crash the app!" - summary = "To test crashes" - onClick { - activity!!.materialAlertDialog() - .setTitle(MR.strings.warning) - .setMessage("I told you this would crash the app, why would you want that?") - .setPositiveButton("Crash it anyway") { _, _ -> throw RuntimeException("Fell into the void") } - .setNegativeButton("Nevermind", null) - .show() + preference { + title = "Crash the app!" + summary = "To test crashes" + onClick { + activity!!.materialAlertDialog() + .setTitle(MR.strings.warning) + .setMessage("I told you this would crash the app, why would you want that?") + .setPositiveButton("Crash it anyway") { _, _ -> throw RuntimeException("Fell into the void") } + .setNegativeButton("Nevermind", null) + .show() + } } - } - preference { - title = "Prune finished workers" - summary = "In case worker stuck in FAILED state and you're too impatient to wait" - onClick { - activity!!.materialAlertDialog() - .setTitle("Are you sure?") - .setMessage( - "Failed workers should clear out by itself eventually, " + - "this option should only be used if you're being impatient and you know what you're doing." - ) - .setPositiveButton("Prune") { _, _ -> context.workManager.pruneWork() } - .setNegativeButton("Cancel", null) - .show() + preference { + title = "Prune finished workers" + summary = "In case worker stuck in FAILED state and you're too impatient to wait" + onClick { + activity!!.materialAlertDialog() + .setTitle("Are you sure?") + .setMessage( + "Failed workers should clear out by itself eventually, " + + "this option should only be used if you're being impatient and you know what you're doing." + ) + .setPositiveButton("Prune") { _, _ -> context.workManager.pruneWork() } + .setNegativeButton("Cancel", null) + .show() + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsBrowseController.kt index 56f4d08b87..7abbb51ba3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsBrowseController.kt @@ -8,6 +8,7 @@ import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R +import yokai.domain.ui.UiPreferences import yokai.i18n.MR import yokai.util.lang.getString import dev.icerock.moko.resources.compose.stringResource @@ -45,6 +46,8 @@ class SettingsBrowseController : SettingsLegacyController() { val sourceManager: SourceManager by injectLazy() var updatedExtNotifPref: SwitchPreferenceCompat? = null + private val uiPreferences: UiPreferences by injectLazy() + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = MR.strings.browse @@ -198,6 +201,15 @@ class SettingsBrowseController : SettingsLegacyController() { infoPreference(MR.strings.you_can_migrate_in_library) } + + preferenceCategory { + titleRes = MR.strings.sources + + switchPreference { + bindTo(uiPreferences.enableSourceSwipeAction()) + titleRes = MR.strings.enable_source_swipe_action + } + } preferenceCategory { titleRes = MR.strings.nsfw_sources diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt index d7d0977e84..77ad3199ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt @@ -34,6 +34,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.domain.category.interactor.GetCategories +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.GetLibraryManga import yokai.domain.ui.UiPreferences import yokai.i18n.MR @@ -48,6 +49,7 @@ class SettingsLibraryController : SettingsLegacyController() { private val getCategories: GetCategories by injectLazy() private val uiPreferences: UiPreferences by injectLazy() + private val libraryPreferences: LibraryPreferences by injectLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = MR.strings.library @@ -212,12 +214,25 @@ class SettingsLibraryController : SettingsLegacyController() { } preferenceCategory { - titleRes = MR.strings.chapters + titleRes = MR.strings.pref_behavior switchPreference { bindTo(uiPreferences.enableChapterSwipeAction()) titleRes = MR.strings.enable_chapter_swipe_action } + multiSelectListPreferenceMat(activity) { + bindTo(libraryPreferences.markDuplicateReadChapterAsRead()) + titleRes = MR.strings.pref_mark_as_read_duplicate_read_chapter + val entries = mapOf( + MR.strings.pref_mark_as_read_duplicate_read_chapter_existing to + LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING, + MR.strings.pref_mark_as_read_duplicate_read_chapter_new to + LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_NEW, + ) + entriesRes = entries.keys.toTypedArray() + entryValues = entries.values.toList() + noSelectionRes = MR.strings.none + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/database/ClearDatabaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/database/ClearDatabaseController.kt index 5c74237139..316589f6cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/database/ClearDatabaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/database/ClearDatabaseController.kt @@ -7,6 +7,8 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Book import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.forEach import androidx.core.view.isInvisible @@ -193,7 +195,7 @@ class ClearDatabaseController : binding.emptyView.hide() } else { binding.emptyView.show( - R.drawable.ic_book_24dp, + Icons.Filled.Book, MR.strings.database_clean, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt index ea69af4080..503b715a41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/debug/DebugController.kt @@ -4,15 +4,15 @@ import android.os.Build import androidx.preference.PreferenceScreen import androidx.webkit.WebViewCompat import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.ui.more.AboutController import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.ui.setting.onClick import eu.kanade.tachiyomi.ui.setting.preference import eu.kanade.tachiyomi.ui.setting.preferenceCategory import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.view.withFadeTransaction -import yokai.i18n.MR import java.text.DateFormat +import yokai.i18n.MR +import yokai.presentation.settings.screen.about.getFormattedBuildTime class DebugController : SettingsLegacyController() { @@ -49,7 +49,7 @@ class DebugController : SettingsLegacyController() { preference { key = "pref_build_time" title = "Build Time" - summary = AboutController.getFormattedBuildTime(dateFormat) + summary = getFormattedBuildTime(dateFormat) } preference { key = "pref_webview_version" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceItem.kt index 14d1d58722..fbf6cd8ca3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceItem.kt @@ -11,6 +11,9 @@ import yokai.util.lang.getString import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.LocalSource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import yokai.domain.ui.UiPreferences /** * Item that contains source information. @@ -29,7 +32,7 @@ class SourceItem(val source: CatalogueSource, header: LangItem? = null, val isPi } override fun isSwipeable(): Boolean { - return source.id != LocalSource.ID && header != null && header.code != SourcePresenter.LAST_USED_KEY + return Injekt.get().enableSourceSwipeAction().get() && source.id != LocalSource.ID && header != null && header.code != SourcePresenter.LAST_USED_KEY } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 8eb367d84b..76aa137449 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -8,6 +8,11 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExploreOff +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.view.WindowInsetsCompat.Type.ime import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.isVisible @@ -44,7 +49,9 @@ import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.util.system.setTextInput import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.activityBinding @@ -54,7 +61,11 @@ import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.isControllerVisible import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.setAction +import eu.kanade.tachiyomi.util.view.setMessage +import eu.kanade.tachiyomi.util.view.setNegativeButton import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener +import eu.kanade.tachiyomi.util.view.setPositiveButton +import eu.kanade.tachiyomi.util.view.setTitle import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.widget.AutofitRecyclerView @@ -62,12 +73,14 @@ import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import kotlin.math.roundToInt import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy import yokai.domain.manga.interactor.GetManga +import yokai.domain.source.browse.filter.models.SavedSearch import yokai.i18n.MR +import yokai.presentation.core.icons.CustomIcons +import yokai.presentation.core.icons.LocalSource import yokai.util.lang.getString /** @@ -137,6 +150,9 @@ open class BrowseSourceController(bundle: Bundle) : private var filterSheet: SourceFilterSheet? = null private var lastPosition: Int = -1 + // Basically a cache just so the filter sheet is shown faster + var savedSearches by mutableStateOf(emptyList()) + private val isBehindGlobalSearch: Boolean get() = router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController @@ -175,11 +191,12 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this, true) + adapter = FlexibleAdapter(null, this, false) setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() binding.fab.setOnClickListener { showFilters() } + activityBinding?.appBar?.y = 0f activityBinding?.appBar?.updateAppBarAfterY(recycler) activityBinding?.appBar?.lockYPos = true @@ -373,12 +390,16 @@ open class BrowseSourceController(bundle: Bundle) : return true } + private fun applyFilters() { + val allDefault = presenter.filtersMatchDefault() + showProgressBar() + adapter?.clear() + presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) + updatePopLatestIcons() + } + private fun showFilters() { if (filterSheet != null) return - val sheet = SourceFilterSheet(activity!!) - filterSheet = sheet - sheet.setFilters(presenter.filterItems) - presenter.filtersChanged = false val oldFilters = mutableListOf() for (i in presenter.sourceFilters) { if (i is Filter.Group<*>) { @@ -391,50 +412,94 @@ open class BrowseSourceController(bundle: Bundle) : oldFilters.add(i.state) } } - sheet.onSearchClicked = { - var matches = true - for (i in presenter.sourceFilters.indices) { - val filter = oldFilters.getOrNull(i) - if (filter is List<*>) { - for (j in filter.indices) { - if (filter[j] != - ( - (presenter.sourceFilters[i] as Filter.Group<*>).state[j] as - Filter<*> - ).state - ) { - matches = false - break - } - } - } else if (filter != presenter.sourceFilters[i].state) { - matches = false - break - } - if (!matches) break - } - if (!matches) { - val allDefault = presenter.filtersMatchDefault() - showProgressBar() - adapter?.clear() - presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) - updatePopLatestIcons() - } - } - sheet.onResetClicked = { - presenter.appliedFilters = FilterList() - val newFilters = presenter.source.getFilterList() - presenter.sourceFilters = newFilters - sheet.setFilters(presenter.filterItems) - } - sheet.setOnDismissListener { - filterSheet = null - } - sheet.setOnCancelListener { - filterSheet = null - } - sheet.show() + filterSheet = SourceFilterSheet( + activity = activity!!, + searches = { savedSearches }, + onSearchClicked = { + var matches = true + for (i in presenter.sourceFilters.indices) { + val filter = oldFilters.getOrNull(i) + if (filter is List<*>) { + for (j in filter.indices) { + if (filter[j] != + ( + (presenter.sourceFilters[i] as Filter.Group<*>).state[j] as + Filter<*> + ).state + ) { + matches = false + break + } + } + } else if (filter != presenter.sourceFilters[i].state) { + matches = false + break + } + if (!matches) break + } + if (!matches) { + applyFilters() + } + }, + onResetClicked = { + presenter.appliedFilters = FilterList() + val newFilters = presenter.source.getFilterList() + presenter.sourceFilters = newFilters + filterSheet?.setFilters(presenter.filterItems) + }, + onSaveClicked = { + viewScope.launchIO { + val names = presenter.loadSearches().map { it.name } + var searchName = "" + withUIContext { + activity!!.materialAlertDialog() + .setTitle(activity!!.getString(MR.strings.save_search)) + .setTextInput(hint = activity!!.getString(MR.strings.save_search_hint)) { input -> + searchName = input + } + .setPositiveButton(MR.strings.save) { _, _ -> + if (searchName.isNotBlank() && searchName !in names) { + presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters) + filterSheet?.scrollToTop() + } else { + activity!!.toast(MR.strings.save_search_invalid_name) + } + } + .setNegativeButton(MR.strings.cancel, null) + .show() + } + } + }, + onSavedSearchClicked = ss@{ searchId -> + viewScope.launchIO { + val search = presenter.loadSearch(searchId) // Grab the latest data from database + if (search?.filters == null) return@launchIO + + withUIContext { + presenter.sourceFilters = search.filters + filterSheet?.setFilters(presenter.filterItems) + // This will call onSaveClicked() + filterSheet?.dismiss() + } + } + }, + onDeleteSavedSearchClicked = { searchId -> + activity!!.materialAlertDialog() + .setTitle(MR.strings.save_search_delete) + .setMessage(MR.strings.save_search_delete) + .setPositiveButton(MR.strings.cancel, null) + .setNegativeButton(android.R.string.ok) { _, _ -> presenter.deleteSearch(searchId) } + .show() + } + ) + filterSheet?.setFilters(presenter.filterItems) + presenter.filtersChanged = false + + filterSheet?.setOnCancelListener { filterSheet = null } + filterSheet?.setOnDismissListener { filterSheet = null } + + filterSheet?.show() } /** @@ -577,7 +642,7 @@ open class BrowseSourceController(bundle: Bundle) : snack?.dismiss() val message = getErrorMessage(error) - val retryAction = View.OnClickListener { + val retryAction = { // If not the first page, show bottom binding.progress bar. if (adapter.mainItemCount > 0 && progressItem != null) { adapter.addScrollableFooterWithDelay(progressItem!!, 0, true) @@ -606,16 +671,16 @@ open class BrowseSourceController(bundle: Bundle) : binding.emptyView.show( if (presenter.source is HttpSource) { - R.drawable.ic_browse_off_24dp + Icons.Filled.ExploreOff } else { - R.drawable.ic_local_library_24dp + CustomIcons.LocalSource }, message, actions, ) } else { snack = binding.sourceLayout.snack(message, Snackbar.LENGTH_INDEFINITE) { - setAction(MR.strings.retry, retryAction) + setAction(MR.strings.retry) { retryAction() } } } if (isControllerVisible) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt index 50ed40ec23..92c31cb860 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt @@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.util.view.setCards import yokai.domain.manga.models.cover -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga /** * Class used to hold the displayed data of a manga in the library, like the cover or the title. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 20b4a8ebe0..d5e4818672 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -66,7 +66,7 @@ class BrowseSourceItem( binding.coverThumbnail.adjustViewBounds = false binding.coverThumbnail.updateLayoutParams { height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT - dimensionRatio = "15:22" + dimensionRatio = "2:3" } } BrowseSourceGridHolder(view, adapter, listType == LibraryItem.LAYOUT_COMPACT_GRID, outlineOnCovers.get()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt index b76cfe7e8a..fcffcab813 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.util.view.setCards import yokai.domain.manga.models.cover -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga /** * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 693e04c267..8ed61311f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.ui.source.filter.TextSectionItem import eu.kanade.tachiyomi.ui.source.filter.TriStateItem import eu.kanade.tachiyomi.ui.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.launchNonCancellableIO import eu.kanade.tachiyomi.util.system.withUIContext import kotlinx.coroutines.Job import kotlinx.coroutines.flow.asFlow @@ -35,8 +36,12 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -44,6 +49,11 @@ import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.models.MangaUpdate +import yokai.domain.source.browse.filter.FilterSerializer +import yokai.domain.source.browse.filter.interactor.DeleteSavedSearch +import yokai.domain.source.browse.filter.interactor.GetSavedSearch +import yokai.domain.source.browse.filter.interactor.InsertSavedSearch +import yokai.domain.source.browse.filter.models.SavedSearch import yokai.domain.ui.UiPreferences // FIXME: Migrate to Compose @@ -63,6 +73,11 @@ open class BrowseSourcePresenter( private val insertManga: InsertManga by injectLazy() private val updateManga: UpdateManga by injectLazy() + private val deleteSavedSearch: DeleteSavedSearch by injectLazy() + private val getSavedSearch: GetSavedSearch by injectLazy() + private val insertSavedSearch: InsertSavedSearch by injectLazy() + private val filterSerializer: FilterSerializer by injectLazy() + /** * Selected source. */ @@ -129,6 +144,15 @@ open class BrowseSourcePresenter( } } filtersChanged = false + + runBlocking { view?.savedSearches = loadSearches() } + + getSavedSearch.subscribeAllBySourceId(sourceId) + .map { it.applyAllSave(source.getFilterList()) } + .onEach { + withUIContext { view?.savedSearches = it } + } + .launchIn(presenterScope) } } @@ -360,4 +384,33 @@ open class BrowseSourcePresenter( } } } + + fun saveSearch(name: String, query: String, filters: FilterList) { + presenterScope.launchNonCancellableIO { + insertSavedSearch.await( + sourceId, + name, + query, + try { + Json.encodeToString(filterSerializer.serialize(filters)) + } catch (e: Exception) { + "[]" + }, + ) + } + } + + fun deleteSearch(searchId: Long) { + presenterScope.launchNonCancellableIO { + deleteSavedSearch.await(searchId) + } + } + + suspend fun loadSearch(id: Long): SavedSearch? { + return getSavedSearch.awaitById(id)?.applySave(source.getFilterList()) + } + + suspend fun loadSearches(): List { + return getSavedSearch.awaitAllBySourceId(sourceId).applyAllSave(source.getFilterList()) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchExtensions.kt new file mode 100644 index 0000000000..d07c2f9430 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchExtensions.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.source.browse + +import eu.kanade.tachiyomi.source.model.FilterList +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import yokai.domain.source.browse.filter.FilterSerializer +import yokai.domain.source.browse.filter.models.RawSavedSearch +import yokai.domain.source.browse.filter.models.SavedSearch + +fun RawSavedSearch.applySave( + originalFilters: FilterList, + json: Json = Injekt.get(), + filterSerializer: FilterSerializer = Injekt.get(), +): SavedSearch { + val rt = SavedSearch( + id = this.id, + name = this.name, + query = this.query.orEmpty(), + filters = null, + ) + if (filtersJson == null) { + return rt + } + + val filters = try { + json.decodeFromString(filtersJson!!) + } catch (e: Exception) { + null + } ?: return rt + + try { + filterSerializer.deserialize(originalFilters, filters) + return rt.copy(filters = originalFilters) + } catch (e: Exception) { + return rt + } +} + +fun List.applyAllSave( + originalFilters: FilterList, + json: Json = Injekt.get(), + filterSerializer: FilterSerializer = Injekt.get(), +) = this.map { it.applySave(originalFilters, json, filterSerializer) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchesAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchesAdapter.kt new file mode 100644 index 0000000000..5dfca7f047 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SavedSearchesAdapter.kt @@ -0,0 +1,93 @@ +package eu.kanade.tachiyomi.ui.source.browse + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.databinding.SourceFilterSheetSavedSearchBinding +import yokai.domain.source.browse.filter.models.SavedSearch +import yokai.presentation.theme.YokaiTheme + +class SavedSearchesAdapter( + val searches: () -> List, + val onSavedSearchClicked: (Long) -> Unit, + val onDeleteSavedSearchClicked: (Long) -> Unit, +) : + RecyclerView.Adapter() { + + private lateinit var binding: SourceFilterSheetSavedSearchBinding + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder { + binding = SourceFilterSheetSavedSearchBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SavedSearchesViewHolder(binding.root) + } + + override fun getItemCount(): Int = 1 + + override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) { + holder.bind() + } + + inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) { + fun bind() { + binding.savedSearches.setContent { + YokaiTheme { + Content() + } + } + binding.savedSearches.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + } + + @Composable + fun Content() { + binding.savedSearchesTitle.isVisible = searches().isNotEmpty() + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + searches().forEach { search -> + val inputChipInteractionSource = remember { MutableInteractionSource() } + Box { + ElevatedSuggestionChip( + label = { Text(search.name) }, + onClick = { }, + interactionSource = inputChipInteractionSource, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors().copy( + containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.4f), + labelColor = MaterialTheme.colorScheme.onSurface, + ), + ) + // Workaround to add long click to chips + Box( + modifier = Modifier + .matchParentSize() + .combinedClickable( + onLongClick = { onDeleteSavedSearchClicked(search.id) }, + onClick = { onSavedSearchClicked(search.id) }, + interactionSource = inputChipInteractionSource, + indication = null, + ) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SourceFilterSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SourceFilterSheet.kt index 14be0b9ae3..2eb00a48df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SourceFilterSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/SourceFilterSheet.kt @@ -6,10 +6,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.withStyledAttributes import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.updateLayoutParams import androidx.core.view.updatePaddingRelative +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import eu.davidea.flexibleadapter.FlexibleAdapter @@ -21,26 +23,37 @@ import eu.kanade.tachiyomi.util.view.checkHeightThen import eu.kanade.tachiyomi.util.view.collapse import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsetsCompat import eu.kanade.tachiyomi.widget.E2EBottomSheetDialog +import yokai.domain.source.browse.filter.models.SavedSearch +import yokai.presentation.component.recyclerview.VertPaddingDecoration import android.R as AR -class SourceFilterSheet(val activity: Activity) : - E2EBottomSheetDialog(activity) { - - private var filterChanged = true +class SourceFilterSheet( + val activity: Activity, + searches: () -> List = { emptyList() }, + val onSearchClicked: () -> Unit, + val onResetClicked: () -> Unit, + val onSaveClicked: () -> Unit, + val onSavedSearchClicked: (Long) -> Unit, + val onDeleteSavedSearchClicked: (Long) -> Unit, +) : E2EBottomSheetDialog(activity) { val adapter: FlexibleAdapter> = FlexibleAdapter>(null) .setDisplayHeadersAtStartUp(true) - var onSearchClicked = {} - - var onResetClicked = {} - override var recyclerView: RecyclerView? = binding.filtersRecycler override fun createBinding(inflater: LayoutInflater) = SourceFilterSheetBinding.inflate(inflater) + + private val savedSearchesAdapter = SavedSearchesAdapter( + searches = searches, + onSavedSearchClicked = onSavedSearchClicked, + onDeleteSavedSearchClicked = onDeleteSavedSearchClicked, + ) + init { binding.searchBtn.setOnClickListener { dismiss() } binding.resetBtn.setOnClickListener { onResetClicked() } + binding.saveBtn.setOnClickListener { onSaveClicked() } sheetBehavior.peekHeight = 450.dpToPx sheetBehavior.collapse() @@ -52,9 +65,7 @@ class SourceFilterSheet(val activity: Activity) : binding.cardView.doOnApplyWindowInsetsCompat { _, insets, _ -> binding.cardView.updateLayoutParams { val fullHeight = activity.window.decorView.height - matchConstraintMaxHeight = - fullHeight - insets.getInsets(systemBars()).top - - binding.titleLayout.height - 75.dpToPx + matchConstraintMaxHeight = fullHeight - insets.getInsets(systemBars()).top - binding.titleLayout.height - 75.dpToPx } } @@ -83,7 +94,7 @@ class SourceFilterSheet(val activity: Activity) : }, ) - binding.filtersRecycler.viewTreeObserver.addOnScrollChangedListener { + recyclerView?.viewTreeObserver?.addOnScrollChangedListener { updateBottomButtons() } @@ -91,10 +102,13 @@ class SourceFilterSheet(val activity: Activity) : updateBottomButtons() } - binding.filtersRecycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) - binding.filtersRecycler.clipToPadding = false - binding.filtersRecycler.adapter = adapter - binding.filtersRecycler.setHasFixedSize(false) + recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + recyclerView?.addItemDecoration(VertPaddingDecoration(12.dpToPx)) + recyclerView?.adapter = ConcatAdapter( + savedSearchesAdapter, + adapter, + ) + recyclerView?.setHasFixedSize(false) sheetBehavior.addBottomSheetCallback( object : BottomSheetBehavior.BottomSheetCallback() { @@ -109,7 +123,7 @@ class SourceFilterSheet(val activity: Activity) : ) } - fun setCardViewMax(insets: WindowInsetsCompat) { + private fun setCardViewMax(insets: WindowInsetsCompat) { val fullHeight = activity.window.decorView.height val newHeight = fullHeight - insets.getInsets(systemBars()).top - binding.titleLayout.height - 75.dpToPx @@ -123,8 +137,10 @@ class SourceFilterSheet(val activity: Activity) : override fun onStart() { super.onStart() sheetBehavior.collapse() + scrollToTop() // Force the sheet to scroll to the very top when it shows up updateBottomButtons() binding.root.post { + scrollToTop() // Force the sheet to scroll to the very top when it shows up updateBottomButtons() } } @@ -132,17 +148,17 @@ class SourceFilterSheet(val activity: Activity) : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val attrsArray = intArrayOf(AR.attr.actionBarSize) - val array = context.obtainStyledAttributes(attrsArray) - val headerHeight = array.getDimensionPixelSize(0, 0) - binding.titleLayout.updatePaddingRelative( - bottom = activity.window.decorView.rootWindowInsetsCompat - ?.getInsets(systemBars())?.bottom ?: 0, - ) + context.withStyledAttributes(null, attrsArray) { + val headerHeight = getDimensionPixelSize(0, 0) + binding.titleLayout.updatePaddingRelative( + bottom = activity.window.decorView.rootWindowInsetsCompat + ?.getInsets(systemBars())?.bottom ?: 0, + ) - binding.titleLayout.updateLayoutParams { - height = headerHeight + binding.titleLayout.paddingBottom + binding.titleLayout.updateLayoutParams { + height = headerHeight + binding.titleLayout.paddingBottom + } } - array.recycle() } private fun updateBottomButtons() { @@ -154,12 +170,14 @@ class SourceFilterSheet(val activity: Activity) : override fun dismiss() { super.dismiss() - if (filterChanged) { - onSearchClicked() - } + onSearchClicked() } fun setFilters(items: List>) { adapter.updateDataSet(items) } + + fun scrollToTop() { + recyclerView?.layoutManager?.scrollToPosition(0) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt index e3837cf493..a2f98a2aa9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.makeShapeCorners import eu.kanade.tachiyomi.util.view.setCards import yokai.domain.manga.models.cover -import yokai.presentation.core.util.coil.loadManga +import yokai.util.coil.loadManga class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) : BaseFlexibleViewHolder(view, adapter) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt index b544928f31..49537791ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt @@ -185,7 +185,7 @@ open class GlobalSearchPresenter( MangasPage(emptyList(), false) } .mangas.take(10) - .map { networkToLocalManga(it, source.id) } + .mapNotNull { networkToLocalManga(it, source.id) } fetchImage(mangas, source) if (mangas.isNotEmpty() && !loadTime.containsKey(source.id)) { loadTime[source.id] = Date().time @@ -275,17 +275,27 @@ open class GlobalSearchPresenter( * @param sManga the manga from the source. * @return a manga from the database. */ - protected open suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + protected open suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga? { var localManga = getManga.awaitByUrlAndSource(sManga.url, sourceId) if (localManga == null) { - val newManga = Manga.create(sManga.url, sManga.title, sourceId) + val newManga = + try { + Manga.create(sManga.url, sManga.title, sourceId) + } catch (_: UninitializedPropertyAccessException) { + return null + } newManga.copyFrom(sManga) newManga.id = insertManga.await(newManga) localManga = newManga } else if (!localManga.favorite) { // if the manga isn't a favorite, set its display title from source // if it later becomes a favorite, updated title will go to db - localManga.title = sManga.title + localManga.title = + try { + sManga.title + } catch (_: UninitializedPropertyAccessException) { + return localManga + } } return localManga } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/WindowSize.kt b/app/src/main/java/eu/kanade/tachiyomi/util/WindowSize.kt new file mode 100644 index 0000000000..311c1f34fd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/WindowSize.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalConfiguration +import eu.kanade.tachiyomi.util.system.isTablet + +@Composable +@ReadOnlyComposable +fun isTablet(): Boolean { + return LocalConfiguration.current.isTablet() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 987d76c91b..54211507c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -17,6 +17,7 @@ import yokai.domain.chapter.interactor.InsertChapter import yokai.domain.chapter.interactor.UpdateChapter import yokai.domain.chapter.models.ChapterUpdate import yokai.domain.chapter.services.ChapterRecognition +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.models.MangaUpdate @@ -39,6 +40,7 @@ suspend fun syncChaptersWithSource( updateChapter: UpdateChapter = Injekt.get(), updateManga: UpdateManga = Injekt.get(), handler: DatabaseHandler = Injekt.get(), + libraryPreferences: LibraryPreferences = Injekt.get(), ): Pair, List> { if (rawSourceChapters.isEmpty()) { throw Exception("No chapters found") @@ -122,11 +124,18 @@ suspend fun syncChaptersWithSource( return Pair(emptyList(), emptyList()) } - val reAdded = mutableListOf() + val changedOrDuplicateReadUrls = mutableSetOf() val deletedChapterNumbers = TreeSet() val deletedReadChapterNumbers = TreeSet() val deletedBookmarkedChapterNumbers = TreeSet() + + val readChapterNumbers = dbChapters + .asSequence() + .filter { it.read && it.isRecognizedNumber } + .map { it.chapter_number } + .toSet() + toDelete.forEach { if (it.read) deletedReadChapterNumbers.add(it.chapter_number) if (it.bookmark) deletedBookmarkedChapterNumbers.add(it.chapter_number) @@ -135,6 +144,9 @@ suspend fun syncChaptersWithSource( val now = Date().time + val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() + .contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_NEW) + // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Sources MUST return the chapters from most to less recent, which is common. var itemCount = toAdd.size @@ -143,6 +155,11 @@ suspend fun syncChaptersWithSource( chapter.date_fetch = now + itemCount-- + if (chapter.chapter_number in readChapterNumbers && markDuplicateAsRead) { + changedOrDuplicateReadUrls.add(chapter.url) + chapter.read = true + } + if (!chapter.isRecognizedNumber || chapter.chapter_number !in deletedChapterNumbers) return@map chapter chapter.read = chapter.chapter_number in deletedReadChapterNumbers @@ -154,7 +171,7 @@ suspend fun syncChaptersWithSource( chapter.date_fetch = it.date_fetch } - reAdded.add(chapter) + changedOrDuplicateReadUrls.add(chapter.url) chapter } @@ -192,14 +209,13 @@ suspend fun syncChaptersWithSource( manga.last_update = Date().time updateManga.await(MangaUpdate(manga.id!!, lastUpdate = manga.last_update)) - val reAddedUrls = reAdded.map { it.url }.toHashSet() val filteredScanlators = ChapterUtil.getScanlators(manga.filtered_scanlators).toHashSet() return Pair( updatedToAdd.filterNot { - it.url in reAddedUrls || it.scanlator in filteredScanlators + it.url in changedOrDuplicateReadUrls || it.scanlator in filteredScanlators }, - toDelete.filterNot { it.url in reAddedUrls }, + toDelete.filterNot { it.url in changedOrDuplicateReadUrls }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt index d5bd0ac97c..33d9efdb30 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt @@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.util.chapter import android.content.Context import android.content.res.ColorStateList import android.widget.TextView +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.core.graphics.ColorUtils import androidx.core.widget.TextViewCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat @@ -19,6 +21,8 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.timeSpanFromNow import java.text.DecimalFormat import java.text.DecimalFormatSymbols +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import yokai.i18n.MR import yokai.util.lang.getString @@ -182,5 +186,12 @@ class ChapterUtil { name } } + + @Composable + fun Chapter.preferredChapterName(manga: Manga): String { + val preferences: PreferencesHelper = Injekt.get() + val context = LocalContext.current + return preferredChapterName(context, manga, preferences) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/compose/Locals.kt b/app/src/main/java/eu/kanade/tachiyomi/util/compose/Locals.kt index d9590305dd..af73accc34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/compose/Locals.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/compose/Locals.kt @@ -4,11 +4,17 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf -import yokai.domain.ComposableAlertDialog +import com.bluelinelabs.conductor.Router +import yokai.domain.DialogHostState val ProvidableCompositionLocal.currentOrThrow @Composable get(): T = this.current ?: throw RuntimeException("CompositionLocal is null") val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } -val LocalAlertDialog: ProvidableCompositionLocal = compositionLocalOf { null } +val LocalDialogHostState: ProvidableCompositionLocal = compositionLocalOf { null } +@Deprecated( + message = "Scheduled for removal once Conductor is fully replaced by Voyager", + replaceWith = ReplaceWith("LocalNavigator", "cafe.adriel.voyager.navigator.LocalNavigator"), +) +val LocalRouter: ProvidableCompositionLocal = compositionLocalOf { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt index 41ed89f945..d8be785576 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt @@ -66,7 +66,12 @@ object MangaCoverMetadata { } else { options.inSampleSize = 4 } - val bitmap = BitmapFactory.decodeFile(file.filePath, options) + val bitmap = try { + val stream = file.openInputStream() + BitmapFactory.decodeStream(stream, null, options) + } catch (_: Throwable) { + null + } if (bitmap != null) { Palette.from(bitmap).generate { palette -> if (isInLibrary) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 0cd311e68c..91943b465d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -47,12 +47,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import rikka.sui.Sui import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.i18n.MR private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720 +private const val TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP = 600 + /** * Helper method to create a notification. * @@ -112,7 +115,8 @@ fun Float.dpToPxEnd(resources: Resources): Float { val Resources.isLTR get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR -fun Context.isTablet() = resources.configuration.smallestScreenWidthDp >= 600 +fun Configuration.isTablet() = smallestScreenWidthDp >= TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP +fun Context.isTablet() = resources.configuration.isTablet() val displayMaxHeightInPx: Int get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } @@ -207,11 +211,13 @@ fun Context.isPackageInstalled(packageName: String): Boolean { return try { packageManager.getApplicationInfoCompat(packageName, 0) true - } catch (_: Exception) { + } catch (_: PackageManager.NameNotFoundException) { false } } +val Context.isShizukuInstalled get() = isPackageInstalled("moe.shizuku.privileged.api") || Sui.isSui() + /** * Property to get the notification manager from the context. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/MaterialAlertDialogExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/MaterialAlertDialogExtensions.kt index a855434180..59b9ccd9df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/MaterialAlertDialogExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/MaterialAlertDialogExtensions.kt @@ -5,16 +5,20 @@ import android.content.DialogInterface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.TextView import androidx.annotation.CheckResult -import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.AppCompatCheckedTextView +import androidx.core.content.getSystemService import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.databinding.CustomDialogTitleMessageBinding import eu.kanade.tachiyomi.databinding.DialogQuadstateBinding +import eu.kanade.tachiyomi.databinding.DialogTextInputBinding import eu.kanade.tachiyomi.widget.TriStateCheckBox import eu.kanade.tachiyomi.widget.materialdialogs.TriStateMultiChoiceDialogAdapter import eu.kanade.tachiyomi.widget.materialdialogs.TriStateMultiChoiceListener @@ -155,3 +159,23 @@ val DialogInterface.isPromptChecked: Boolean fun interface MaterialAlertDialogBuilderOnCheckClickListener { fun onClick(var1: DialogInterface?, var3: Boolean) } + +fun MaterialAlertDialogBuilder.setTextInput( + hint: String? = null, + prefill: String? = null, + onTextChanged: (String) -> Unit, +): MaterialAlertDialogBuilder { + val binding = DialogTextInputBinding.inflate(LayoutInflater.from(context)) + binding.textField.hint = hint + binding.textField.editText?.apply { + setText(prefill, TextView.BufferType.EDITABLE) + doAfterTextChanged { + onTextChanged(it?.toString() ?: "") + } + post { + requestFocusFromTouch() + context.getSystemService()?.showSoftInput(this, 0) + } + } + return setView(binding.root) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt index 83ce972fe8..cb5ec8e234 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ThemeUtil.kt @@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.util.system import android.content.Context import android.content.res.Resources -import android.graphics.Color import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.core.content.edit import androidx.core.view.WindowInsetsControllerCompat import androidx.preference.PreferenceManager @@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.reader.settings.ReaderBackgroundColor import uy.kohesive.injekt.injectLazy +import android.graphics.Color as AColor object ThemeUtil { @@ -47,20 +49,21 @@ object ThemeUtil { return context.isInNightMode() && preferences.themeDarkAmoled().get() } - fun readerBackgroundColor(theme: Int, default: Int = Color.WHITE): Int { + fun readerBackgroundColor(theme: Int, default: Int = AColor.WHITE): Int { return when (ReaderBackgroundColor.fromPreference(theme)) { - ReaderBackgroundColor.GRAY -> Color.rgb(32, 33, 37) - ReaderBackgroundColor.BLACK -> Color.BLACK - ReaderBackgroundColor.WHITE -> Color.WHITE + ReaderBackgroundColor.GRAY -> AColor.rgb(32, 33, 37) + ReaderBackgroundColor.BLACK -> AColor.BLACK + ReaderBackgroundColor.WHITE -> AColor.WHITE else -> default } } - fun readerContentColor(theme: Int, default: Int = Color.BLACK): Int { + @Composable + fun readerContentColor(theme: Int, default: Color = Color.Black): Color { return when (ReaderBackgroundColor.fromPreference(theme)) { - ReaderBackgroundColor.GRAY -> Color.WHITE - ReaderBackgroundColor.BLACK -> Color.WHITE - ReaderBackgroundColor.WHITE -> Color.BLACK + ReaderBackgroundColor.GRAY -> Color.White + ReaderBackgroundColor.BLACK -> Color.White + ReaderBackgroundColor.WHITE -> Color.Black else -> default } } @@ -78,7 +81,7 @@ fun AppCompatActivity.getThemeWithExtras(theme: Resources.Theme, preferences: Pr if (oldTheme != null && useAmoled) { val array = oldTheme.obtainStyledAttributes(intArrayOf(R.attr.background)) val bg = array.getColor(0, 0) - if (bg == Color.BLACK) { + if (bg == AColor.BLACK) { return oldTheme } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt index 6bbaa86939..c62ddbcaa8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt @@ -2,28 +2,51 @@ package eu.kanade.tachiyomi.widget import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.RelativeLayout -import androidx.annotation.DrawableRes +import android.view.Gravity +import android.widget.FrameLayout import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.google.android.material.button.MaterialButton import dev.icerock.moko.resources.StringResource -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.CommonViewEmptyBinding -import eu.kanade.tachiyomi.util.view.setText -import eu.kanade.tachiyomi.util.view.setVectorCompat +import eu.kanade.tachiyomi.util.isTablet +import yokai.presentation.component.EmptyScreen +import yokai.presentation.theme.YokaiTheme import yokai.util.lang.getString -import android.R as AR -class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - RelativeLayout(context, attrs) { +class EmptyView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : AbstractComposeView(context, attrs, defStyleAttr) { - private val binding: CommonViewEmptyBinding = - CommonViewEmptyBinding.inflate(LayoutInflater.from(context), this, true) + private var image by mutableStateOf(Icons.Filled.Download) + private var message by mutableStateOf("") + private var actions by mutableStateOf(emptyList()) + + init { + layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + } + + @Composable + override fun Content() { + YokaiTheme { + EmptyScreen( + image = image, + message = message, + isTablet = isTablet(), + actions = actions, + ) + } + } /** * Hide the information view @@ -36,16 +59,16 @@ class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? * Show the information view * @param textResource text of information view */ - fun show(@DrawableRes drawable: Int, textResource: StringResource, actions: List? = null) { - show(drawable, context.getString(textResource), actions) + fun show(image: ImageVector, textResource: StringResource, actions: List = emptyList()) { + show(image, context.getString(textResource), actions) } /** * Show the information view * @param textResource text of information view */ - fun show(@DrawableRes drawable: Int, @StringRes textResource: Int, actions: List? = null) { - show(drawable, context.getString(textResource), actions) + fun show(image: ImageVector, @StringRes textResource: Int, actions: List = emptyList()) { + show(image, context.getString(textResource), actions) } /** @@ -53,36 +76,15 @@ class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? * @param drawable icon of information view * @param textResource text of information view */ - fun show(@DrawableRes drawable: Int, message: String, actions: List? = null) { - binding.imageView.setVectorCompat(drawable, AR.attr.textColorHint) - binding.textLabel.text = message - - binding.actionsContainer.removeAllViews() - binding.actionsContainer.isVisible = !actions.isNullOrEmpty() - if (!actions.isNullOrEmpty()) { - actions.forEach { - val button = - (inflate(context, R.layout.material_text_button, null) as MaterialButton) - .apply { - setText(it.resId) - setOnClickListener(it.listener) - } - binding.actionsContainer.addView(button) - if (context.resources.configuration.screenHeightDp < 600) { - button.textAlignment = View.TEXT_ALIGNMENT_TEXT_START - button.updateLayoutParams { - width = ViewGroup.LayoutParams.WRAP_CONTENT - height = ViewGroup.LayoutParams.WRAP_CONTENT - } - } - } - } - + fun show(image: ImageVector, message: String, actions: List = emptyList()) { + this.image = image + this.message = message + this.actions = actions this.isVisible = true } data class Action( val resId: StringResource, - val listener: OnClickListener, + val listener: () -> Unit, ) } diff --git a/app/src/main/java/yokai/core/di/AppModule.kt b/app/src/main/java/yokai/core/di/AppModule.kt index f53d7c1e1a..70f1f13d92 100644 --- a/app/src/main/java/yokai/core/di/AppModule.kt +++ b/app/src/main/java/yokai/core/di/AppModule.kt @@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.chapter.ChapterFilter import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import kotlinx.serialization.json.Json +import kotlinx.serialization.protobuf.ProtoBuf import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML @@ -145,6 +146,9 @@ fun appModule(app: Application) = module { xmlVersion = XmlVersion.XML10 } } + single { + ProtoBuf + } single { ChapterFilter() } diff --git a/app/src/main/java/yokai/core/di/DomainModule.kt b/app/src/main/java/yokai/core/di/DomainModule.kt index 31e98fc4fa..406e6457db 100644 --- a/app/src/main/java/yokai/core/di/DomainModule.kt +++ b/app/src/main/java/yokai/core/di/DomainModule.kt @@ -7,6 +7,7 @@ import yokai.data.extension.repo.ExtensionRepoRepositoryImpl import yokai.data.history.HistoryRepositoryImpl import yokai.data.library.custom.CustomMangaRepositoryImpl import yokai.data.manga.MangaRepositoryImpl +import yokai.data.source.browse.filter.SavedSearchRepositoryImpl import yokai.data.track.TrackRepositoryImpl import yokai.domain.category.CategoryRepository import yokai.domain.category.interactor.DeleteCategories @@ -42,6 +43,11 @@ import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.UpdateManga import yokai.domain.recents.interactor.GetRecents +import yokai.domain.source.browse.filter.FilterSerializer +import yokai.domain.source.browse.filter.SavedSearchRepository +import yokai.domain.source.browse.filter.interactor.DeleteSavedSearch +import yokai.domain.source.browse.filter.interactor.GetSavedSearch +import yokai.domain.source.browse.filter.interactor.InsertSavedSearch import yokai.domain.track.TrackRepository import yokai.domain.track.interactor.DeleteTrack import yokai.domain.track.interactor.GetTrack @@ -95,4 +101,10 @@ fun domainModule() = module { factory { DeleteTrack(get()) } factory { GetTrack(get()) } factory { InsertTrack(get()) } + + single { SavedSearchRepositoryImpl(get()) } + factory { DeleteSavedSearch(get()) } + factory { GetSavedSearch(get()) } + factory { InsertSavedSearch(get()) } + factory { FilterSerializer() } } diff --git a/app/src/main/java/yokai/core/di/PreferenceModule.kt b/app/src/main/java/yokai/core/di/PreferenceModule.kt index b309579312..a9cf3a5c46 100644 --- a/app/src/main/java/yokai/core/di/PreferenceModule.kt +++ b/app/src/main/java/yokai/core/di/PreferenceModule.kt @@ -13,6 +13,7 @@ import org.koin.dsl.module import yokai.domain.backup.BackupPreferences import yokai.domain.base.BasePreferences import yokai.domain.download.DownloadPreferences +import yokai.domain.library.LibraryPreferences import yokai.domain.recents.RecentsPreferences import yokai.domain.source.SourcePreferences import yokai.domain.storage.StoragePreferences @@ -47,6 +48,8 @@ fun preferenceModule(application: Application) = module { single { BackupPreferences(get()) } + single { LibraryPreferences(get()) } + single { PreferencesHelper( context = application, diff --git a/app/src/main/java/yokai/domain/ComposableAlertDialog.kt b/app/src/main/java/yokai/domain/ComposableAlertDialog.kt deleted file mode 100644 index 9be777dd38..0000000000 --- a/app/src/main/java/yokai/domain/ComposableAlertDialog.kt +++ /dev/null @@ -1,10 +0,0 @@ -package yokai.domain - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue - -class ComposableAlertDialog(initial: (@Composable () -> Unit)?) { - var content: (@Composable () -> Unit)? by mutableStateOf(initial) -} diff --git a/app/src/main/java/yokai/domain/DialogHostState.kt b/app/src/main/java/yokai/domain/DialogHostState.kt new file mode 100644 index 0000000000..4c53b460c8 --- /dev/null +++ b/app/src/main/java/yokai/domain/DialogHostState.kt @@ -0,0 +1,28 @@ +package yokai.domain + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +typealias ComposableDialog = (@Composable () -> Unit)? +typealias ComposableDialogState = MutableState + +class DialogHostState(initial: ComposableDialog = null) : ComposableDialogState by mutableStateOf(initial) { + val mutex = Mutex() + + fun closeDialog() { + value = null + } + + suspend inline fun dialog(crossinline dialog: @Composable (CancellableContinuation) -> Unit) = mutex.withLock { + try { + suspendCancellableCoroutine { cont -> value = { dialog(cont) } } + } finally { + closeDialog() + } + } +} diff --git a/app/src/main/java/yokai/domain/base/BasePreferences.kt b/app/src/main/java/yokai/domain/base/BasePreferences.kt index 5e450ee513..ca8e21ded1 100644 --- a/app/src/main/java/yokai/domain/base/BasePreferences.kt +++ b/app/src/main/java/yokai/domain/base/BasePreferences.kt @@ -49,4 +49,6 @@ class BasePreferences(private val preferenceStore: PreferenceStore) { } fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT) + + fun composeLibrary() = preferenceStore.getBoolean("pref_use_compose_library", false) } diff --git a/app/src/main/java/yokai/domain/extension/repo/service/ExtensionRepoService.kt b/app/src/main/java/yokai/domain/extension/repo/service/ExtensionRepoService.kt index b07f5d0468..2613d71f38 100644 --- a/app/src/main/java/yokai/domain/extension/repo/service/ExtensionRepoService.kt +++ b/app/src/main/java/yokai/domain/extension/repo/service/ExtensionRepoService.kt @@ -6,17 +6,13 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.system.withIOContext -import kotlinx.serialization.json.Json import okhttp3.OkHttpClient -import uy.kohesive.injekt.injectLazy import yokai.domain.extension.repo.model.ExtensionRepo class ExtensionRepoService( private val client: OkHttpClient, ) { - private val json: Json by injectLazy() - suspend fun fetchRepoDetails( repo: String, ): ExtensionRepo? { @@ -24,12 +20,10 @@ class ExtensionRepoService( val url = "$repo/repo.json".toUri() try { - with(json) { - client.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() - .toExtensionRepo(baseUrl = repo) - } + client.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .toExtensionRepo(baseUrl = repo) } catch (e: Exception) { Logger.e(e) { "Failed to fetch repo details" } null diff --git a/app/src/main/java/yokai/domain/library/LibraryPreferences.kt b/app/src/main/java/yokai/domain/library/LibraryPreferences.kt new file mode 100644 index 0000000000..ab1094de0f --- /dev/null +++ b/app/src/main/java/yokai/domain/library/LibraryPreferences.kt @@ -0,0 +1,14 @@ +package yokai.domain.library + +import eu.kanade.tachiyomi.core.preference.PreferenceStore + +class LibraryPreferences(private val preferenceStore: PreferenceStore) { + fun randomSortSeed() = preferenceStore.getInt("library_random_sort_seed", 0) + + fun markDuplicateReadChapterAsRead() = preferenceStore.getStringSet("mark_duplicate_read_chapter_read", emptySet()) + + companion object { + const val MARK_DUPLICATE_READ_CHAPTER_READ_NEW = "new" + const val MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING = "existing" + } +} diff --git a/app/src/main/java/yokai/domain/library/custom/model/CustomMangaInfo.kt b/app/src/main/java/yokai/domain/library/custom/model/CustomMangaInfo.kt index 9076e6c3b8..789dd4be01 100644 --- a/app/src/main/java/yokai/domain/library/custom/model/CustomMangaInfo.kt +++ b/app/src/main/java/yokai/domain/library/custom/model/CustomMangaInfo.kt @@ -12,8 +12,7 @@ data class CustomMangaInfo( val genre: String? = null, val status: Int? = null, ) { - fun toManga() = MangaImpl().apply { - id = this@CustomMangaInfo.mangaId + fun toManga() = MangaImpl(id = this.mangaId).apply { title = this@CustomMangaInfo.title ?: "" author = this@CustomMangaInfo.author artist = this@CustomMangaInfo.artist diff --git a/app/src/main/java/yokai/domain/recents/interactor/GetRecents.kt b/app/src/main/java/yokai/domain/recents/interactor/GetRecents.kt index 9cf8c41fe6..2ca0923d17 100644 --- a/app/src/main/java/yokai/domain/recents/interactor/GetRecents.kt +++ b/app/src/main/java/yokai/domain/recents/interactor/GetRecents.kt @@ -55,4 +55,13 @@ class GetRecents( return historyRepository.getRecentsAll(includeRead, filterScanlators, search, limit, actualOffset) } + + suspend fun awaitUpdates(limit: Long = 0L): List = + historyRepository.getRecentsAll( + includeRead = false, + filterScanlators = true, + search = "", + limit = limit, + offset = 0L + ) } diff --git a/app/src/main/java/yokai/domain/source/browse/filter/FilterSerializer.kt b/app/src/main/java/yokai/domain/source/browse/filter/FilterSerializer.kt new file mode 100644 index 0000000000..ec7e66a12f --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/FilterSerializer.kt @@ -0,0 +1,94 @@ +package yokai.domain.source.browse.filter + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.isSubclassOf +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +class FilterSerializer { + private val serializers = listOf>( + HeaderSerializer(this), + SeparatorSerializer(this), + SelectSerializer(this), + TextSerializer(this), + CheckBoxSerializer(this), + TriStateSerializer(this), + GroupSerializer(this), + SortSerializer(this), + ) + + fun serialize(filters: FilterList) = buildJsonArray { + filters.filterIsInstance>().forEach { add(serialize(it)) } + } + + fun serialize(filter: Filter): JsonObject { + return serializers + .filterIsInstance>>() + .firstOrNull { filter::class.isSubclassOf(it.clazz) } + ?.let { serializer -> + buildJsonObject { + with(serializer) { serialize(filter) } + + serializer.mappings().forEach { + val res = it.second.get(filter) + putJsonObject(it.first) { + put(Serializer.TYPE, res?.javaClass?.name ?: "null") + put("value", res.toString()) + } + } + + put(Serializer.TYPE, serializer.type) + } + } ?: throw IllegalArgumentException("Cannot serialize this Filter object!") + } + + fun deserialize(filters: FilterList, json: JsonArray) { + filters.filterIsInstance>().zip(json).forEach { (filter, obj) -> + deserialize(filter, obj.jsonObject) + } + } + + fun deserialize(filter: Filter, json: JsonObject) { + val serializer = serializers + .filterIsInstance>>() + .firstOrNull { it.type == json[Serializer.TYPE]!!.jsonPrimitive.content } + ?: throw IllegalArgumentException("Cannot deserialize this type!") + + serializer.deserialize(json, filter) + + serializer.mappings().forEach { + if (it.second is KMutableProperty1) { + val valueObj = json[it.first]!!.jsonObject + val obj = valueObj["value"]!!.jsonPrimitive + val res: Any? = when (valueObj[Serializer.TYPE]!!.jsonPrimitive.content) { + java.lang.Integer::class.java.name -> obj.int + java.lang.Long::class.java.name -> obj.long + java.lang.Float::class.java.name -> obj.float + java.lang.Double::class.java.name -> obj.double + java.lang.String::class.java.name -> obj.content + java.lang.Boolean::class.java.name -> obj.boolean + java.lang.Byte::class.java.name -> obj.content.toByte() + java.lang.Short::class.java.name -> obj.content.toShort() + java.lang.Character::class.java.name -> obj.content[0] + "null" -> null + else -> throw IllegalArgumentException("Cannot deserialize this type!") + } + @Suppress("UNCHECKED_CAST") + (it.second as KMutableProperty1, in Any?>).set(filter, res) + } + } + } +} diff --git a/app/src/main/java/yokai/domain/source/browse/filter/FilterTypeSerializer.kt b/app/src/main/java/yokai/domain/source/browse/filter/FilterTypeSerializer.kt new file mode 100644 index 0000000000..817765b643 --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/FilterTypeSerializer.kt @@ -0,0 +1,187 @@ +package yokai.domain.source.browse.filter + +import eu.kanade.tachiyomi.source.model.Filter +import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.add +import kotlinx.serialization.json.addAll +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject + +interface Serializer> { + fun JsonObjectBuilder.serialize(filter: T) {} + fun deserialize(json: JsonObject, filter: T) {} + + fun mappings(): List>> = emptyList() + + val serializer: FilterSerializer + val type: String + val clazz: KClass + + companion object { + const val TYPE = "_type" + const val NAME = "name" + const val STATE = "state" + } +} + +class HeaderSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "HEADER" + override val clazz = Filter.Header::class + + override fun mappings() = listOf( + Serializer.NAME to Filter.Header::name, + ) +} + +class SeparatorSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "SEPARATOR" + override val clazz = Filter.Separator::class + + override fun mappings() = listOf( + Serializer.NAME to Filter.Separator::name, + ) +} + +class SelectSerializer(override val serializer: FilterSerializer) : Serializer> { + override val type = "SELECT" + override val clazz = Filter.Select::class + + override fun JsonObjectBuilder.serialize(filter: Filter.Select) { + putJsonArray("values") { + addAll(filter.values.map { it.toString() }) + } + } + + override fun mappings() = listOf( + Serializer.NAME to Filter.Select::name, + Serializer.STATE to Filter.Select::state, + ) +} + +class TextSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "TEXT" + override val clazz = Filter.Text::class + + override fun mappings() = listOf( + Serializer.NAME to Filter.Text::name, + Serializer.STATE to Filter.Text::state, + ) +} + +class CheckBoxSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "CHECKBOX" + override val clazz = Filter.CheckBox::class + + override fun mappings() = listOf( + Serializer.NAME to Filter.CheckBox::name, + Serializer.STATE to Filter.CheckBox::state, + ) +} + +class TriStateSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "TRI_STATE" + override val clazz = Filter.TriState::class + + override fun mappings() = listOf( + Serializer.NAME to Filter.TriState::name, + Serializer.STATE to Filter.TriState::state, + ) +} + +class GroupSerializer(override val serializer: FilterSerializer) : Serializer> { + override val type = "GROUP" + override val clazz = Filter.Group::class + + override fun JsonObjectBuilder.serialize(filter: Filter.Group) { + putJsonObject(VALUES) { + filter.state.forEach { item -> + @Suppress("UNCHECKED_CAST", "SafeCastWithReturn") + item as? Filter ?: return@forEach + // Assuming `item.name` is unique, not sure if it is tho... + put(item.name, serializer.serialize(item)) + } + } + } + + override fun deserialize(json: JsonObject, filter: Filter.Group) { + val values = json[VALUES]?.jsonObject + if (values == null) { + // TODO: Delete later + json[Serializer.STATE]?.jsonArray?.forEachIndexed { index, element -> + if (element == JsonNull) return@forEachIndexed + + @Suppress("UNCHECKED_CAST") + serializer.deserialize(filter.state[index] as Filter, element.jsonObject) + } + return + } + + filter.state.forEach { item -> + @Suppress("UNCHECKED_CAST", "SafeCastWithReturn") + item as? Filter ?: return@forEach + + val itemJson = values[item.name]?.jsonObject ?: return@forEach + serializer.deserialize(item, itemJson) + } + } + + override fun mappings() = listOf( + Serializer.NAME to Filter.Group::name, + ) + + companion object { + const val VALUES = "values" + } +} + +class SortSerializer(override val serializer: FilterSerializer) : Serializer { + override val type = "SORT" + override val clazz = Filter.Sort::class + + override fun JsonObjectBuilder.serialize(filter: Filter.Sort) { + putJsonArray(VALUES) { + filter.values.forEach { add(it) } + } + + put( + Serializer.STATE, + filter.state?.let { (index, ascending) -> + buildJsonObject { + put(STATE_INDEX, index) + put(STATE_ASCENDING, ascending) + } + } ?: JsonNull, + ) + } + + override fun deserialize(json: JsonObject, filter: Filter.Sort) { + filter.state = (json[Serializer.STATE] as? JsonObject)?.let { + Filter.Sort.Selection( + it[STATE_INDEX]!!.jsonPrimitive.int, + it[STATE_ASCENDING]!!.jsonPrimitive.boolean, + ) + } + } + + override fun mappings() = listOf( + Pair(Serializer.NAME, Filter.Sort::name), + ) + + companion object { + const val VALUES = "values" + + const val STATE_INDEX = "index" + const val STATE_ASCENDING = "ascending" + } +} diff --git a/app/src/main/java/yokai/domain/source/browse/filter/interactor/DeleteSavedSearch.kt b/app/src/main/java/yokai/domain/source/browse/filter/interactor/DeleteSavedSearch.kt new file mode 100644 index 0000000000..6c460bca89 --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/interactor/DeleteSavedSearch.kt @@ -0,0 +1,9 @@ +package yokai.domain.source.browse.filter.interactor + +import yokai.domain.source.browse.filter.SavedSearchRepository + +class DeleteSavedSearch( + private val repository: SavedSearchRepository, +) { + suspend fun await(searchId: Long) = repository.deleteById(searchId) +} diff --git a/app/src/main/java/yokai/domain/source/browse/filter/interactor/GetSavedSearch.kt b/app/src/main/java/yokai/domain/source/browse/filter/interactor/GetSavedSearch.kt new file mode 100644 index 0000000000..da8a8fd5c6 --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/interactor/GetSavedSearch.kt @@ -0,0 +1,13 @@ +package yokai.domain.source.browse.filter.interactor + +import yokai.domain.source.browse.filter.SavedSearchRepository + +class GetSavedSearch( + private val repository: SavedSearchRepository, +) { + suspend fun awaitAll() = repository.findAll() + suspend fun awaitAllBySourceId(sourceId: Long) = repository.findAllBySourceId(sourceId) + fun subscribeAllBySourceId(sourceId: Long) = repository.subscribeAllBySourceId(sourceId) + suspend fun awaitBySourceIdAndName(sourceId: Long, name: String) = repository.findOneBySourceIdAndName(sourceId, name) + suspend fun awaitById(id: Long) = repository.findById(id) +} diff --git a/app/src/main/java/yokai/domain/source/browse/filter/interactor/InsertSavedSearch.kt b/app/src/main/java/yokai/domain/source/browse/filter/interactor/InsertSavedSearch.kt new file mode 100644 index 0000000000..eb8e6ee08d --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/interactor/InsertSavedSearch.kt @@ -0,0 +1,9 @@ +package yokai.domain.source.browse.filter.interactor + +import yokai.domain.source.browse.filter.SavedSearchRepository + +class InsertSavedSearch( + private val repository: SavedSearchRepository, +) { + suspend fun await(sourceId: Long, name: String, query: String?, filtersJson: String?) = repository.insert(sourceId, name, query, filtersJson) +} diff --git a/app/src/main/java/yokai/domain/source/browse/filter/models/SavedSearch.kt b/app/src/main/java/yokai/domain/source/browse/filter/models/SavedSearch.kt new file mode 100644 index 0000000000..d18e8ad2c2 --- /dev/null +++ b/app/src/main/java/yokai/domain/source/browse/filter/models/SavedSearch.kt @@ -0,0 +1,10 @@ +package yokai.domain.source.browse.filter.models + +import eu.kanade.tachiyomi.source.model.FilterList + +data class SavedSearch( + val id: Long, + val name: String, + val query: String, + val filters: FilterList?, +) diff --git a/app/src/main/java/yokai/domain/ui/UiPreferences.kt b/app/src/main/java/yokai/domain/ui/UiPreferences.kt index 3d35eee75a..1e73ad2eea 100644 --- a/app/src/main/java/yokai/domain/ui/UiPreferences.kt +++ b/app/src/main/java/yokai/domain/ui/UiPreferences.kt @@ -11,4 +11,6 @@ class UiPreferences(private val preferenceStore: PreferenceStore) { fun uniformGrid() = preferenceStore.getBoolean(PreferenceKeys.uniformGrid, true) fun enableChapterSwipeAction() = preferenceStore.getBoolean("enable_chapter_swipe_action", true) + + fun enableSourceSwipeAction() = preferenceStore.getBoolean("enable_source_swipe_action", true) } diff --git a/app/src/main/java/yokai/presentation/Scaffold.kt b/app/src/main/java/yokai/presentation/Scaffold.kt index 4b29a2a373..4c63800a1e 100644 --- a/app/src/main/java/yokai/presentation/Scaffold.kt +++ b/app/src/main/java/yokai/presentation/Scaffold.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -28,34 +27,37 @@ import androidx.core.view.WindowInsetsControllerCompat import dev.icerock.moko.resources.compose.stringResource import yokai.i18n.MR import yokai.presentation.component.ToolTipButton +import yokai.presentation.core.ExpandedAppBar @Composable fun YokaiScaffold( onNavigationIconClicked: () -> Unit, modifier: Modifier = Modifier, title: String = "", - scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState()), + scrollBehavior: TopAppBarScrollBehavior? = null, fab: @Composable () -> Unit = {}, navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack, navigationIconLabel: String = stringResource(MR.strings.back), actions: @Composable RowScope.() -> Unit = {}, appBarType: AppBarType = AppBarType.LARGE, + snackbarHost: @Composable () -> Unit = {}, content: @Composable (PaddingValues) -> Unit, ) { + val scrollBehaviorOrDefault = scrollBehavior ?: TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState()) val view = LocalView.current val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5 - val color = getTopAppBarColor(title) + val (color, scrolledColor) = getTopAppBarColor(title) SideEffect { val activity = view.context as Activity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.window.statusBarColor = color.toArgb() + activity.window.statusBarColor = Color.Transparent.toArgb() WindowInsetsControllerCompat(activity.window, view).isAppearanceLightStatusBars = useDarkIcons } } Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier.nestedScroll(scrollBehaviorOrDefault.nestedScrollConnection), floatingActionButton = fab, topBar = { when (appBarType) { @@ -66,7 +68,7 @@ fun YokaiScaffold( // modifier = Modifier.statusBarsPadding(), colors = topAppBarColors( containerColor = color, - scrolledContainerColor = color, + scrolledContainerColor = scrolledColor, ), navigationIcon = { ToolTipButton( @@ -75,17 +77,17 @@ fun YokaiScaffold( buttonClicked = onNavigationIconClicked, ) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollBehaviorOrDefault, actions = actions, ) - AppBarType.LARGE -> LargeTopAppBar( + AppBarType.LARGE -> ExpandedAppBar( title = { Text(text = title) }, // modifier = Modifier.statusBarsPadding(), colors = topAppBarColors( containerColor = color, - scrolledContainerColor = color, + scrolledContainerColor = scrolledColor, ), navigationIcon = { ToolTipButton( @@ -94,24 +96,28 @@ fun YokaiScaffold( buttonClicked = onNavigationIconClicked, ) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollBehaviorOrDefault, actions = actions, ) + AppBarType.NONE -> {} } }, + snackbarHost = snackbarHost, content = content, ) } @Composable -fun getTopAppBarColor(title: String): Color { +fun getTopAppBarColor(title: String): Pair { return when (title.isEmpty()) { - true -> Color.Transparent - false -> MaterialTheme.colorScheme.surface + true -> Color.Transparent to Color.Transparent + false -> MaterialTheme.colorScheme.surface to MaterialTheme.colorScheme.primaryContainer } } enum class AppBarType { + // FIXME: Delete "NONE" later + NONE, SMALL, LARGE, } diff --git a/app/src/main/java/yokai/presentation/component/EmptyScreen.kt b/app/src/main/java/yokai/presentation/component/EmptyScreen.kt index dc9d4c66df..06d49c6ee6 100644 --- a/app/src/main/java/yokai/presentation/component/EmptyScreen.kt +++ b/app/src/main/java/yokai/presentation/component/EmptyScreen.kt @@ -4,6 +4,8 @@ import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -13,15 +15,21 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.util.compose.textHint +import eu.kanade.tachiyomi.widget.EmptyView +import yokai.i18n.MR private val defaultIconModifier = Modifier.size(128.dp) @@ -34,8 +42,9 @@ fun EmptyScreen( modifier: Modifier = Modifier, image: ImageVector, message: String, - actions: @Composable () -> Unit = {}, -) = EmptyScreen( + isTablet: Boolean, + actions: List = emptyList(), +) = EmptyScreenImpl( modifier = modifier, image = { Image( @@ -46,7 +55,8 @@ fun EmptyScreen( ) }, message = message, - actions = actions, + actions = { EmptyScreenActions(actions, isTablet) }, + isTablet = isTablet, ) @Composable @@ -54,8 +64,9 @@ fun EmptyScreen( modifier: Modifier = Modifier, image: ImageBitmap, message: String, - actions: @Composable () -> Unit = {}, -) = EmptyScreen( + isTablet: Boolean, + actions: List = emptyList(), +) = EmptyScreenImpl( modifier = modifier, image = { Image( @@ -65,35 +76,106 @@ fun EmptyScreen( ) }, message = message, - actions = actions, + actions = { EmptyScreenActions(actions, isTablet) }, + isTablet = isTablet, ) -@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true) @Composable -private fun EmptyScreen( - modifier: Modifier = Modifier, - image: @Composable () -> Unit = { - Image(modifier = defaultIconModifier, imageVector = Icons.Filled.Download, contentDescription = null) - }, - message: String = "Something went wrong", - actions: @Composable () -> Unit = {}, -) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - image() - Text( - modifier = Modifier - .padding(top = 16.dp), - text = message, - color = MaterialTheme.colorScheme.textHint, - style = MaterialTheme.typography.labelMedium, - ) - actions() +private fun EmptyScreenActions(actions: List, isTablet: Boolean) { + if (isTablet) { + FlowRow { + actions.forEach { action -> + TextButton(onClick = { action.listener() }) { + Text( + text = stringResource(action.resId), + fontSize = 14.sp, + ) + } + } + } + } else { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + actions.forEach { action -> + TextButton(onClick = { action.listener() }) { + Text( + text = stringResource(action.resId), + fontSize = 14.sp, + ) + } + } + } } } + +@Composable +private fun EmptyScreenImpl( + modifier: Modifier = Modifier, + image: @Composable () -> Unit, + message: String, + actions: @Composable () -> Unit, + isTablet: Boolean, +) { + if (isTablet) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Row { + image() + Text( + modifier = Modifier + .padding(vertical = 4.dp), + text = message, + color = MaterialTheme.colorScheme.textHint, + style = MaterialTheme.typography.labelMedium, + ) + } + actions() + } + } else { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + image() + Text( + modifier = Modifier + .padding(vertical = 16.dp), + text = message, + color = MaterialTheme.colorScheme.textHint, + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + ) + actions() + } + } +} + +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true) +@Composable +private fun EmptyScreenPreview() { + EmptyScreen( + image = Icons.Filled.Download, + message = "Something went wrong", + actions = listOf( + EmptyView.Action(MR.strings.download) {}, + EmptyView.Action(MR.strings.download) {}, + EmptyView.Action(MR.strings.download) {}, + EmptyView.Action(MR.strings.download) {}, + EmptyView.Action(MR.strings.download) {}, + ), + isTablet = false, + ) +} diff --git a/app/src/main/java/yokai/presentation/component/TrackLogoIcon.kt b/app/src/main/java/yokai/presentation/component/TrackLogoIcon.kt index ca75eb8d7b..3a426e678a 100644 --- a/app/src/main/java/yokai/presentation/component/TrackLogoIcon.kt +++ b/app/src/main/java/yokai/presentation/component/TrackLogoIcon.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.data.track.TrackService -import yokai.presentation.core.util.clickableNoIndication +import yokai.util.clickableNoIndication @Composable fun TrackLogoIcon( diff --git a/app/src/main/java/yokai/presentation/component/preference/widget/InfoWidget.kt b/app/src/main/java/yokai/presentation/component/preference/widget/InfoWidget.kt index 334cfd0dde..833e034f9e 100644 --- a/app/src/main/java/yokai/presentation/component/preference/widget/InfoWidget.kt +++ b/app/src/main/java/yokai/presentation/component/preference/widget/InfoWidget.kt @@ -10,8 +10,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import yokai.presentation.core.util.secondaryItemAlpha import yokai.presentation.theme.Size +import yokai.util.secondaryItemAlpha @Composable internal fun InfoWidget(text: String) { diff --git a/app/src/main/java/yokai/presentation/component/preference/widget/TextPreferenceWidget.kt b/app/src/main/java/yokai/presentation/component/preference/widget/TextPreferenceWidget.kt index 8a4a677d76..4ed6ba6e6d 100644 --- a/app/src/main/java/yokai/presentation/component/preference/widget/TextPreferenceWidget.kt +++ b/app/src/main/java/yokai/presentation/component/preference/widget/TextPreferenceWidget.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import yokai.presentation.core.util.secondaryItemAlpha +import yokai.util.secondaryItemAlpha @Composable fun TextPreferenceWidget( diff --git a/app/src/main/java/yokai/presentation/component/recyclerview/VertPaddingDecoration.kt b/app/src/main/java/yokai/presentation/component/recyclerview/VertPaddingDecoration.kt new file mode 100644 index 0000000000..6aba55167d --- /dev/null +++ b/app/src/main/java/yokai/presentation/component/recyclerview/VertPaddingDecoration.kt @@ -0,0 +1,25 @@ +package yokai.presentation.component.recyclerview + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +/** + * Add (vertical) padding to RecyclerView first and last item. Because `clipToPadding = "false"` bugged out for Bottom Sheets. + */ +class VertPaddingDecoration(private val padding: Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + + val itemPosition = parent.getChildAdapterPosition(view) + + if (itemPosition == RecyclerView.NO_POSITION) return; + + when { + itemPosition == 0 -> + outRect.top = padding + itemPosition > 0 && itemPosition == state.itemCount - 1 -> + outRect.bottom = padding + } + } +} diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt index f99b28c24a..f2e8cd923a 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoController.kt @@ -1,31 +1,22 @@ package yokai.presentation.extension.repo import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.CrossfadeTransition import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController -import eu.kanade.tachiyomi.util.compose.LocalAlertDialog -import eu.kanade.tachiyomi.util.compose.LocalBackPress -import yokai.domain.ComposableAlertDialog -class ExtensionRepoController() : - BaseComposeController() { - - private var repoUrl: String? = null - - constructor(repoUrl: String) : this() { - this.repoUrl = repoUrl - } +class ExtensionRepoController(private val repoUrl: String? = null) : BaseComposeController() { @Composable override fun ScreenContent() { - CompositionLocalProvider( - LocalAlertDialog provides ComposableAlertDialog(null), - LocalBackPress provides router::handleBack, - ) { - ExtensionRepoScreen( + Navigator( + screen = ExtensionRepoScreen( title = "Extension Repos", repoUrl = repoUrl, - ) - } + ), + content = { + CrossfadeTransition(navigator = it) + }, + ) } } diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt index 47a756a7bb..b85255cd00 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreen.kt @@ -19,19 +19,22 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import cafe.adriel.voyager.core.model.rememberScreenModel import dev.icerock.moko.resources.compose.stringResource -import eu.kanade.tachiyomi.util.compose.LocalAlertDialog import eu.kanade.tachiyomi.util.compose.LocalBackPress +import eu.kanade.tachiyomi.util.compose.LocalDialogHostState import eu.kanade.tachiyomi.util.compose.currentOrThrow +import eu.kanade.tachiyomi.util.isTablet import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.collectLatest -import yokai.domain.ComposableAlertDialog +import kotlinx.coroutines.launch +import yokai.domain.DialogHostState import yokai.domain.extension.repo.model.ExtensionRepo import yokai.i18n.MR import yokai.presentation.AppBarType @@ -40,186 +43,197 @@ import yokai.presentation.component.EmptyScreen import yokai.presentation.component.ToolTipButton import yokai.presentation.extension.repo.component.ExtensionRepoInput import yokai.presentation.extension.repo.component.ExtensionRepoItem +import yokai.util.Screen import android.R as AR -@Composable -fun ExtensionRepoScreen( - title: String, - viewModel: ExtensionRepoViewModel = viewModel(), - repoUrl: String? = null, -) { - val onBackPress = LocalBackPress.currentOrThrow - val context = LocalContext.current - val repoState by viewModel.repoState.collectAsState() - var inputText by remember { mutableStateOf("") } - val listState = rememberLazyListState() - val alertDialog = LocalAlertDialog.currentOrThrow +class ExtensionRepoScreen( + private val title: String, + private var repoUrl: String? = null, +): Screen() { + @Composable + override fun Content() { + val onBackPress = LocalBackPress.currentOrThrow + val context = LocalContext.current + val alertDialog = LocalDialogHostState.currentOrThrow - YokaiScaffold( - onNavigationIconClicked = onBackPress, - title = title, - appBarType = AppBarType.SMALL, - scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( - state = rememberTopAppBarState(), - canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 }, - ), - actions = { - ToolTipButton( - toolTipLabel = stringResource(MR.strings.refresh), - icon = Icons.Outlined.Refresh, - buttonClicked = { - context.toast("Refreshing...") // TODO: Should be loading animation instead - viewModel.refreshRepos() - }, - ) - }, - ) { innerPadding -> - if (repoState is ExtensionRepoState.Loading) return@YokaiScaffold + val scope = rememberCoroutineScope() + val screenModel = rememberScreenModel { ExtensionRepoScreenModel() } + val state by screenModel.state.collectAsState() - val repos = (repoState as ExtensionRepoState.Success).repos + var inputText by remember { mutableStateOf("") } + val listState = rememberLazyListState() - alertDialog.content?.let { it() } - - LazyColumn( - modifier = Modifier.padding(innerPadding), - userScrollEnabled = true, - verticalArrangement = Arrangement.Top, - state = listState, - ) { - item { - ExtensionRepoInput( - inputText = inputText, - inputHint = stringResource(MR.strings.label_add_repo), - onInputChange = { inputText = it }, - onAddClick = { viewModel.addRepo(it) }, + YokaiScaffold( + onNavigationIconClicked = onBackPress, + title = title, + appBarType = AppBarType.SMALL, + scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 }, + ), + actions = { + ToolTipButton( + toolTipLabel = stringResource(MR.strings.refresh), + icon = Icons.Outlined.Refresh, + buttonClicked = { + context.toast("Refreshing...") // TODO: Should be loading animation instead + screenModel.refreshRepos() + }, ) - } + }, + ) { innerPadding -> + if (state is ExtensionRepoScreenModel.State.Loading) return@YokaiScaffold - if (repos.isEmpty()) { + val repos = (state as ExtensionRepoScreenModel.State.Success).repos + + alertDialog.value?.invoke() + + LazyColumn( + modifier = Modifier.padding(innerPadding), + userScrollEnabled = true, + verticalArrangement = Arrangement.Top, + state = listState, + ) { item { - EmptyScreen( - modifier = Modifier.fillParentMaxSize(), - image = Icons.Filled.ExtensionOff, - message = stringResource(MR.strings.information_empty_repos), + ExtensionRepoInput( + inputText = inputText, + inputHint = stringResource(MR.strings.label_add_repo), + onInputChange = { inputText = it }, + onAddClick = { screenModel.addRepo(it) }, ) } - return@LazyColumn - } - repos.forEach { repo -> - item { - ExtensionRepoItem( - extensionRepo = repo, - onDeleteClick = { repoToDelete -> - alertDialog.content = { ExtensionRepoDeletePrompt(repoToDelete, alertDialog, viewModel) } - }, - ) + if (repos.isEmpty()) { + item { + EmptyScreen( + modifier = Modifier.fillParentMaxSize(), + image = Icons.Filled.ExtensionOff, + message = stringResource(MR.strings.information_empty_repos), + isTablet = isTablet(), + ) + } + return@LazyColumn } - } - } - } - LaunchedEffect(repoUrl) { - repoUrl?.let { viewModel.addRepo(repoUrl) } - } - - LaunchedEffect(Unit) { - viewModel.event.collectLatest { event -> - if (event is ExtensionRepoEvent.LocalizedMessage) - context.toast(event.stringRes) - if (event is ExtensionRepoEvent.Success) - inputText = "" - if (event is ExtensionRepoEvent.ShowDialog) - alertDialog.content = { - if (event.dialog is RepoDialog.Conflict) { - ExtensionRepoReplacePrompt( - oldRepo = event.dialog.oldRepo, - newRepo = event.dialog.newRepo, - onDismissRequest = { alertDialog.content = null }, - onMigrate = { viewModel.replaceRepo(event.dialog.newRepo) }, + repos.forEach { repo -> + item { + ExtensionRepoItem( + extensionRepo = repo, + onDeleteClick = { repoToDelete -> + scope.launch { alertDialog.awaitExtensionRepoDeletePrompt(repoToDelete, screenModel) } + }, ) } } + } + } + + LaunchedEffect(repoUrl) { + repoUrl?.let { + screenModel.addRepo(repoUrl!!) + repoUrl = null + } + } + + LaunchedEffect(Unit) { + screenModel.event.collectLatest { event -> + when (event) { + is ExtensionRepoEvent.NoOp -> {} + is ExtensionRepoEvent.LocalizedMessage -> context.toast(event.stringRes) + is ExtensionRepoEvent.Success -> inputText = "" + is ExtensionRepoEvent.ShowDialog -> { + when(event.dialog) { + is RepoDialog.Conflict -> { + alertDialog.awaitExtensionRepoReplacePrompt( + oldRepo = event.dialog.oldRepo, + newRepo = event.dialog.newRepo, + onMigrate = { screenModel.replaceRepo(event.dialog.newRepo) }, + ) + } + } + } + } + } } } -} -@Composable -fun ExtensionRepoReplacePrompt( - oldRepo: ExtensionRepo, - newRepo: ExtensionRepo, - onDismissRequest: () -> Unit, - onMigrate: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = { - onMigrate() - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_replace_repo)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(AR.string.cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_replace_repo_title)) - }, - text = { - Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) - }, - ) -} - -@Composable -fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlertDialog, viewModel: ExtensionRepoViewModel) { - AlertDialog( - containerColor = MaterialTheme.colorScheme.surface, - title = { - Text( - text = stringResource(MR.strings.confirm_delete_repo_title), - fontStyle = MaterialTheme.typography.titleMedium.fontStyle, - color = MaterialTheme.colorScheme.onSurface, - fontSize = 24.sp, - ) - }, - text = { - Text( - text = stringResource(MR.strings.confirm_delete_repo, repoToDelete), - fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp, - ) - }, - onDismissRequest = { alertDialog.content = null }, - confirmButton = { - TextButton( - onClick = { - viewModel.deleteRepo(repoToDelete) - alertDialog.content = null + private suspend fun DialogHostState.awaitExtensionRepoReplacePrompt( + oldRepo: ExtensionRepo, + newRepo: ExtensionRepo, + onMigrate: () -> Unit, + ): Unit = dialog { cont -> + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton( + onClick = { + onMigrate() + cont.cancel() + }, + ) { + Text(text = stringResource(MR.strings.action_replace_repo)) } - ) { + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text(text = stringResource(AR.string.cancel)) + } + }, + title = { + Text(text = stringResource(MR.strings.action_replace_repo_title)) + }, + text = { + Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) + }, + ) + } + + private suspend fun DialogHostState.awaitExtensionRepoDeletePrompt( + repoToDelete: String, + screenModel: ExtensionRepoScreenModel, + ): Unit = dialog { cont -> + AlertDialog( + containerColor = MaterialTheme.colorScheme.surface, + title = { Text( - text = stringResource(MR.strings.delete), - color = MaterialTheme.colorScheme.primary, + text = stringResource(MR.strings.confirm_delete_repo_title), + fontStyle = MaterialTheme.typography.titleMedium.fontStyle, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 24.sp, + ) + }, + text = { + Text( + text = stringResource(MR.strings.confirm_delete_repo, repoToDelete), + fontStyle = MaterialTheme.typography.bodyMedium.fontStyle, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 14.sp, ) - } - }, - dismissButton = { - TextButton(onClick = { alertDialog.content = null }) { - Text( - text = stringResource(MR.strings.cancel), - color = MaterialTheme.colorScheme.primary, - fontSize = 14.sp, - ) - } - }, - ) + }, + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton( + onClick = { + screenModel.deleteRepo(repoToDelete) + cont.cancel() + } + ) { + Text( + text = stringResource(MR.strings.delete), + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + ) + } + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text( + text = stringResource(MR.strings.cancel), + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + ) + } + }, + ) + } } diff --git a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt similarity index 77% rename from app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt rename to app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt index 58f3a6518a..372f13edc2 100644 --- a/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt +++ b/app/src/main/java/yokai/presentation/extension/repo/ExtensionRepoScreenModel.kt @@ -1,8 +1,8 @@ package yokai.presentation.extension.repo import androidx.compose.runtime.Immutable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.util.system.launchIO @@ -22,8 +22,7 @@ import yokai.domain.extension.repo.interactor.UpdateExtensionRepo import yokai.domain.extension.repo.model.ExtensionRepo import yokai.i18n.MR -class ExtensionRepoViewModel : - ViewModel() { +class ExtensionRepoScreenModel : StateScreenModel(State.Loading) { private val extensionManager: ExtensionManager by injectLazy() @@ -33,23 +32,20 @@ class ExtensionRepoViewModel : private val replaceExtensionRepo: ReplaceExtensionRepo by injectLazy() private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() - private val mutableRepoState: MutableStateFlow = MutableStateFlow(ExtensionRepoState.Loading) - val repoState: StateFlow = mutableRepoState.asStateFlow() - private val internalEvent: MutableStateFlow = MutableStateFlow(ExtensionRepoEvent.NoOp) val event: StateFlow = internalEvent.asStateFlow() init { - viewModelScope.launchIO { + screenModelScope.launchIO { getExtensionRepo.subscribeAll().collectLatest { repos -> - mutableRepoState.update { ExtensionRepoState.Success(repos = repos.toImmutableList()) } + mutableState.update { State.Success(repos = repos.toImmutableList()) } extensionManager.refreshTrust() } } } fun addRepo(url: String) { - viewModelScope.launchIO { + screenModelScope.launchIO { when (val result = createExtensionRepo.await(url)) { is CreateExtensionRepo.Result.Success -> internalEvent.value = ExtensionRepoEvent.Success is CreateExtensionRepo.Result.Error -> internalEvent.value = ExtensionRepoEvent.InvalidUrl @@ -63,26 +59,41 @@ class ExtensionRepoViewModel : } fun replaceRepo(newRepo: ExtensionRepo) { - viewModelScope.launchIO { + screenModelScope.launchIO { replaceExtensionRepo.await(newRepo) } } fun refreshRepos() { - val status = repoState.value + val status = state.value - if (status is ExtensionRepoState.Success) { - viewModelScope.launchIO { + if (status is State.Success) { + screenModelScope.launchIO { updateExtensionRepo.awaitAll() } } } fun deleteRepo(url: String) { - viewModelScope.launchIO { + screenModelScope.launchIO { deleteExtensionRepo.await(url) } } + + sealed interface State { + + @Immutable + data object Loading : State + + @Immutable + data class Success( + val repos: ImmutableList, + ) : State { + + val isEmpty: Boolean + get() = repos.isEmpty() + } + } } sealed class RepoDialog { @@ -98,17 +109,3 @@ sealed class ExtensionRepoEvent { data object Success : ExtensionRepoEvent() } -sealed class ExtensionRepoState { - - @Immutable - data object Loading : ExtensionRepoState() - - @Immutable - data class Success( - val repos: ImmutableList, - ) : ExtensionRepoState() { - - val isEmpty: Boolean - get() = repos.isEmpty() - } -} diff --git a/app/src/main/java/yokai/presentation/library/LibraryContent.kt b/app/src/main/java/yokai/presentation/library/LibraryContent.kt new file mode 100644 index 0000000000..b334a2e25c --- /dev/null +++ b/app/src/main/java/yokai/presentation/library/LibraryContent.kt @@ -0,0 +1,45 @@ +package yokai.presentation.library + +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import eu.kanade.tachiyomi.ui.library.models.LibraryItem +import yokai.presentation.AppBarType +import yokai.presentation.YokaiScaffold +import yokai.presentation.library.components.LazyLibraryGrid + +@Composable +fun LibraryContent( + modifier: Modifier = Modifier, + items: List, + columns: Int, +) { + YokaiScaffold( + onNavigationIconClicked = {}, + appBarType = AppBarType.NONE, + ) { contentPadding -> + LazyLibraryGrid( + modifier = modifier, + columns = columns, + contentPadding = contentPadding, + ) { + items( + items = items, + contentType = { "library_grid_item" } + ) { item -> + when (item) { + is LibraryItem.Blank -> { + Text("Blank: ${item.mangaCount}") + } + is LibraryItem.Hidden -> { + Text("Hidden: ${item.title} - ${item.hiddenItems.size}") + } + is LibraryItem.Manga -> { + Text("Manga: ${item.libraryManga.manga.title}") + } + } + } + } + } +} diff --git a/app/src/main/java/yokai/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/yokai/presentation/library/components/CommonMangaItem.kt new file mode 100644 index 0000000000..cec16949c2 --- /dev/null +++ b/app/src/main/java/yokai/presentation/library/components/CommonMangaItem.kt @@ -0,0 +1,11 @@ +package yokai.presentation.library.components + +import androidx.compose.ui.unit.dp + +object CommonMangaItemDefaults { + val GridHorizontalSpacer = 4.dp + val GridVerticalSpacer = 4.dp + + @Suppress("ConstPropertyName") + const val BrowseFavoriteCoverAlpha = 0.34f +} diff --git a/app/src/main/java/yokai/presentation/library/components/LazyLibraryGrid.kt b/app/src/main/java/yokai/presentation/library/components/LazyLibraryGrid.kt new file mode 100644 index 0000000000..ae4541db99 --- /dev/null +++ b/app/src/main/java/yokai/presentation/library/components/LazyLibraryGrid.kt @@ -0,0 +1,28 @@ +package yokai.presentation.library.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import yokai.presentation.core.components.FastScrollLazyVerticalGrid +import yokai.presentation.core.util.plus + +@Composable +internal fun LazyLibraryGrid( + modifier: Modifier = Modifier, + columns: Int, + contentPadding: PaddingValues, + content: LazyGridScope.() -> Unit, +) { + FastScrollLazyVerticalGrid( + columns = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns), + modifier = modifier, + contentPadding = contentPadding + PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridVerticalSpacer), + horizontalArrangement = Arrangement.spacedBy(CommonMangaItemDefaults.GridHorizontalSpacer), + content = content, + ) +} diff --git a/app/src/main/java/yokai/presentation/onboarding/InfoScreen.kt b/app/src/main/java/yokai/presentation/onboarding/InfoScreen.kt index d26622c5dc..7671157a80 100644 --- a/app/src/main/java/yokai/presentation/onboarding/InfoScreen.kt +++ b/app/src/main/java/yokai/presentation/onboarding/InfoScreen.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.zIndex -import yokai.presentation.core.util.secondaryItemAlpha import yokai.presentation.theme.Size +import yokai.util.secondaryItemAlpha @Composable fun InfoScreen( diff --git a/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt b/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt index 056660af2e..27596046f2 100644 --- a/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt +++ b/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt @@ -21,22 +21,18 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.core.content.getSystemService -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect import dev.icerock.moko.resources.compose.stringResource +import eu.kanade.tachiyomi.util.system.isShizukuInstalled +import yokai.i18n.MR import yokai.presentation.component.Gap import yokai.presentation.theme.Size @@ -52,35 +48,26 @@ internal class PermissionStep : OnboardingStep { @Composable override fun Content() { val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner.lifecycle) { - val observer = object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - installGranted = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.packageManager.canRequestPackageInstalls() - } else { - @Suppress("DEPRECATION") - Settings.Secure.getInt( - context.contentResolver, - Settings.Secure.INSTALL_NON_MARKET_APPS - ) != 0 - } - notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED - } else { - true - } - batteryGranted = context.getSystemService()!! - .isIgnoringBatteryOptimizations(context.packageName) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + installGranted = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.packageManager.canRequestPackageInstalls() + } else { + @Suppress("DEPRECATION") + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.INSTALL_NON_MARKET_APPS + ) != 0 + } || context.isShizukuInstalled + notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } else { + true } + batteryGranted = context.getSystemService()!! + .isIgnoringBatteryOptimizations(context.packageName) } Column( diff --git a/app/src/main/java/yokai/presentation/reader/ChapterTransition.kt b/app/src/main/java/yokai/presentation/reader/ChapterTransition.kt new file mode 100644 index 0000000000..f51879ce76 --- /dev/null +++ b/app/src/main/java/yokai/presentation/reader/ChapterTransition.kt @@ -0,0 +1,300 @@ +package yokai.presentation.reader + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.CardColors +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.icerock.moko.resources.compose.pluralStringResource +import dev.icerock.moko.resources.compose.stringResource +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition +import eu.kanade.tachiyomi.ui.reader.viewer.missingChapterCount +import eu.kanade.tachiyomi.util.chapter.ChapterUtil.Companion.preferredChapterName +import kotlinx.collections.immutable.persistentMapOf +import yokai.i18n.MR +import yokai.util.secondaryItemAlpha + +@Composable +fun ChapterTransition( + manga: Manga, + transition: ChapterTransition, + currChapterDownloaded: Boolean, + goingToChapterDownloaded: Boolean, +) { + val currChapter = transition.from.chapter + val goingToChapter = transition.to?.chapter + val chapterGap = missingChapterCount(transition) + + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + when (transition) { + is ChapterTransition.Prev -> { + TransitionText( + manga = manga, + topLabel = stringResource(MR.strings.previous_title), + topChapter = goingToChapter, + topChapterDownloaded = goingToChapterDownloaded, + bottomLabel = stringResource(MR.strings.current_chapter), + bottomChapter = currChapter, + bottomChapterDownloaded = currChapterDownloaded, + fallbackLabel = stringResource(MR.strings.theres_no_previous_chapter), + chapterGap = chapterGap, + ) + } + is ChapterTransition.Next -> { + TransitionText( + manga = manga, + topLabel = stringResource(MR.strings.finished_chapter), + topChapter = currChapter, + topChapterDownloaded = currChapterDownloaded, + bottomLabel = stringResource(MR.strings.next_title), + bottomChapter = goingToChapter, + bottomChapterDownloaded = goingToChapterDownloaded, + fallbackLabel = stringResource(MR.strings.theres_no_next_chapter), + chapterGap = chapterGap, + ) + } + } + } +} + +@Composable +private fun TransitionText( + manga: Manga, + topLabel: String, + topChapter: Chapter?, + topChapterDownloaded: Boolean, + bottomLabel: String, + bottomChapter: Chapter?, + bottomChapterDownloaded: Boolean, + fallbackLabel: String, + chapterGap: Int, +) { + Column ( + modifier = Modifier + .widthIn(max = 460.dp) + .fillMaxWidth(), + ) { + if (topChapter != null) { + ChapterText( + header = topLabel, + name = topChapter.preferredChapterName(manga), + scanlator = topChapter.scanlator, + otherDownloaded = bottomChapterDownloaded, + downloaded = topChapterDownloaded, + ) + + Spacer(Modifier.height(VerticalSpacerSize)) + } else { + NoChapterNotification( + text = fallbackLabel, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + + if (bottomChapter != null) { + if (chapterGap > 0) { + ChapterGapWarning( + gapCount = chapterGap, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + + Spacer(Modifier.height(VerticalSpacerSize)) + + ChapterText( + header = bottomLabel, + name = bottomChapter.preferredChapterName(manga), + scanlator = bottomChapter.scanlator, + otherDownloaded = topChapterDownloaded, + downloaded = bottomChapterDownloaded, + ) + } else { + NoChapterNotification( + text = fallbackLabel, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + } +} + +@Composable +private fun NoChapterNotification( + text: String, + modifier: Modifier = Modifier, +) { + OutlinedCard ( + modifier = modifier, + colors = CardColor, + ) { + Row ( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Info, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, + ) + + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} + +@Composable +private fun ChapterGapWarning( + gapCount: Int, + modifier: Modifier = Modifier, +) { + OutlinedCard( + modifier = modifier, + colors = CardColor, + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Warning, + tint = MaterialTheme.colorScheme.error, + contentDescription = null, + ) + + Text( + text = pluralStringResource(MR.plurals.missing_chapters_warning, quantity = gapCount, gapCount), + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} + +@Composable +private fun ChapterHeaderText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + modifier = modifier, + style = MaterialTheme.typography.titleMedium, + ) +} + +@Composable +private fun ChapterText( + header: String, + name: String, + scanlator: String?, + otherDownloaded: Boolean, + downloaded: Boolean, +) { + Column { + ChapterHeaderText( + text = header, + modifier = Modifier.padding(bottom = 4.dp), + ) + + Text( + text = buildAnnotatedString { + if (downloaded || otherDownloaded) { + if (downloaded) { + appendInlineContent(DOWNLOADED_ICON_ID) + } else { + appendInlineContent(ONLINE_ICON_ID) + } + append(' ') + } + append(name) + }, + fontSize = 20.sp, + maxLines = 5, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + inlineContent = persistentMapOf( + DOWNLOADED_ICON_ID to InlineTextContent( + Placeholder( + width = 22.sp, + height = 22.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = stringResource(MR.strings.downloaded), + ) + }, + ONLINE_ICON_ID to InlineTextContent( + Placeholder( + width = 22.sp, + height = 22.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + imageVector = Icons.Filled.Cloud, + contentDescription = stringResource(MR.strings.not_downloaded), + ) + }, + ), + ) + + scanlator?.let { + Text( + text = it, + modifier = Modifier + .secondaryItemAlpha() + .padding(top = 2.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} + +private val CardColor: CardColors + @Composable + get() = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + contentColor = LocalContentColor.current, + ) + +private val VerticalSpacerSize = 24.dp +private const val DOWNLOADED_ICON_ID = "downloaded" +private const val ONLINE_ICON_ID = "online" diff --git a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt index 3d6cd91655..34feac3f93 100644 --- a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt +++ b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -17,9 +17,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import eu.kanade.tachiyomi.core.storage.preference.collectAsState import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.util.compose.LocalAlertDialog import eu.kanade.tachiyomi.util.compose.LocalBackPress +import eu.kanade.tachiyomi.util.compose.LocalDialogHostState import eu.kanade.tachiyomi.util.compose.currentOrThrow +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import uy.kohesive.injekt.injectLazy import yokai.presentation.AppBarType @@ -28,7 +29,36 @@ import yokai.presentation.component.Gap import yokai.presentation.component.preference.Preference import yokai.presentation.component.preference.PreferenceItem import yokai.presentation.component.preference.widget.PreferenceGroupHeader -import kotlin.time.Duration.Companion.seconds +import yokai.presentation.core.drawVerticalScrollbar +import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior + +@Composable +fun SettingsScaffold( + title: String, + appBarType: AppBarType? = null, + appBarActions: @Composable RowScope.() -> Unit = {}, + appBarScrollBehavior: TopAppBarScrollBehavior? = null, + snackbarHost: @Composable () -> Unit = {}, + content: @Composable (PaddingValues) -> Unit, +) { + val preferences: PreferencesHelper by injectLazy() + val useLargeAppBar by preferences.useLargeToolbar().collectAsState() + val onBackPress = LocalBackPress.currentOrThrow + val alertDialog = LocalDialogHostState.currentOrThrow + + YokaiScaffold( + onNavigationIconClicked = onBackPress, + title = title, + appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL, + actions = appBarActions, + scrollBehavior = appBarScrollBehavior, + snackbarHost = snackbarHost, + ) { innerPadding -> + alertDialog.value?.invoke() + + content(innerPadding) + } +} @Composable fun SettingsScaffold( @@ -37,24 +67,18 @@ fun SettingsScaffold( appBarActions: @Composable RowScope.() -> Unit = {}, itemsProvider: @Composable () -> List, ) { - val preferences: PreferencesHelper by injectLazy() - val useLargeAppBar by preferences.useLargeToolbar().collectAsState() val listState = rememberLazyListState() - val onBackPress = LocalBackPress.currentOrThrow - val alertDialog = LocalAlertDialog.currentOrThrow - YokaiScaffold( - onNavigationIconClicked = onBackPress, + SettingsScaffold( title = title, - appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL, - actions = appBarActions, - scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + appBarType = appBarType, + appBarActions = appBarActions, + appBarScrollBehavior = enterAlwaysCollapsedScrollBehavior( state = rememberTopAppBarState(), canScroll = { listState.canScrollForward || listState.canScrollBackward }, + isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }, ), ) { innerPadding -> - alertDialog.content?.let { it() } - PreferenceScreen( items = itemsProvider(), listState = listState, @@ -83,7 +107,7 @@ fun PreferenceScreen( } LazyColumn( - modifier = modifier, + modifier = modifier.drawVerticalScrollbar(listState), contentPadding = contentPadding, state = listState ) { diff --git a/app/src/main/java/yokai/presentation/settings/screen/SettingsDataScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/SettingsDataScreen.kt index 0ac654961a..4fba963c21 100644 --- a/app/src/main/java/yokai/presentation/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/SettingsDataScreen.kt @@ -33,11 +33,10 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.util.compose.LocalAlertDialog +import eu.kanade.tachiyomi.util.compose.LocalDialogHostState import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.relativeTimeSpanString import eu.kanade.tachiyomi.util.system.DeviceUtil -import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.launchNonCancellableIO import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.withUIContext @@ -57,9 +56,9 @@ import yokai.presentation.component.preference.storageLocationText import yokai.presentation.component.preference.widget.BasePreferenceWidget import yokai.presentation.component.preference.widget.PrefsHorizontalPadding import yokai.presentation.settings.ComposableSettings -import yokai.presentation.settings.screen.data.CreateBackup -import yokai.presentation.settings.screen.data.RestoreBackup import yokai.presentation.settings.screen.data.StorageInfo +import yokai.presentation.settings.screen.data.awaitCreateBackup +import yokai.presentation.settings.screen.data.awaitRestoreBackup import yokai.presentation.settings.screen.data.storageLocationPicker import yokai.util.lang.getString @@ -101,7 +100,7 @@ object SettingsDataScreen : ComposableSettings { private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { val scope = rememberCoroutineScope() val context = LocalContext.current - val alertDialog = LocalAlertDialog.currentOrThrow + val alertDialog = LocalDialogHostState.currentOrThrow val extensionManager = remember { Injekt.get() } val storageManager = remember { Injekt.get() } @@ -122,14 +121,11 @@ object SettingsDataScreen : ComposableSettings { Pair(null, e) } - alertDialog.content = { - RestoreBackup( + scope.launch { + alertDialog.awaitRestoreBackup( context = context, uri = it, pair = results, - onDismissRequest = { - alertDialog.content = null - } ) } } @@ -166,11 +162,10 @@ object SettingsDataScreen : ComposableSettings { return@SegmentedButton } - alertDialog.content = { - CreateBackup( + scope.launch { + alertDialog.awaitCreateBackup( context = context, uri = dir.uri, - onDismissRequest = { alertDialog.content = null }, ) } } else { diff --git a/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt new file mode 100644 index 0000000000..8fb10c81a8 --- /dev/null +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt @@ -0,0 +1,108 @@ +package yokai.presentation.settings.screen.about + +import android.os.Build +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import com.google.android.material.textview.MaterialTextView +import dev.icerock.moko.resources.compose.stringResource +import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob +import eu.kanade.tachiyomi.ui.more.parseReleaseNotes +import java.io.Serializable +import kotlin.coroutines.resume +import yokai.domain.DialogHostState +import yokai.i18n.MR +import android.R as AR + +data class NewUpdateData( + val body: String, + val url: String, + val isBeta: Boolean?, +) : Serializable + +suspend fun DialogHostState.awaitNewUpdateDialog( + data: NewUpdateData, + onDismiss: () -> Unit = {}, +): Unit = dialog { cont -> + val context = LocalContext.current + val appContext = context.applicationContext + + val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + AlertDialog( + onDismissRequest = { + onDismiss() + cont.cancel() + }, + title = { + Text( + text = stringResource( + if (data.isBeta == true) { + MR.strings.new_beta_version_available + } else { + MR.strings.new_version_available + } + ) + ) + }, + confirmButton = { + TextButton(onClick = { + AppDownloadInstallJob.start(appContext, data.url, true) + onDismiss() + cont.cancel() + }) { + Text(text = stringResource(if (isOnA12) MR.strings.update else MR.strings.download)) + } + }, + dismissButton = { + TextButton( + onClick = { + onDismiss() + cont.cancel() + } + ) { + Text(text = stringResource(MR.strings.ignore)) + } + }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + MarkdownText(data.body) + } + } + ) +} + +@Composable +private fun MarkdownText(text: String) { + val context = LocalContext.current + AndroidView( + factory = { + MaterialTextView(it) + }, + update = { + it.text = context.parseReleaseNotes(text) + }, + ) +} + +suspend fun DialogHostState.awaitNotificationPermissionDeniedDialog(): Unit = dialog { cont -> + // cont.resume(Unit) so that new update dialog will be shown next + AlertDialog( + onDismissRequest = { if (cont.isActive) cont.resume(Unit) }, + title = { Text(text = stringResource(MR.strings.warning)) }, + text = { Text(text = stringResource(MR.strings.allow_notifications_recommended)) }, + confirmButton = { + TextButton(onClick = { if (cont.isActive) cont.resume(Unit) }) { + Text(text = stringResource(AR.string.ok)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt rename to app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt index 11b2e7c66d..7ed11d10d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLibraryLicenseScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLibraryLicenseScreen.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.more +package yokai.presentation.settings.screen.about import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt rename to app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt index c98bc48cfa..6d4ca8be2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutLicenseScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutLicenseScreen.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.more +package yokai.presentation.settings.screen.about import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.TopAppBarDefaults diff --git a/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt new file mode 100644 index 0000000000..77ff7c7cdd --- /dev/null +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt @@ -0,0 +1,297 @@ +package yokai.presentation.settings.screen.about + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import cafe.adriel.voyager.navigator.LocalNavigator +import co.touchlab.kermit.Logger +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.stringResource +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.core.storage.preference.asDateFormat +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.updater.AppUpdateChecker +import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier +import eu.kanade.tachiyomi.data.updater.AppUpdateResult +import eu.kanade.tachiyomi.data.updater.RELEASE_URL +import eu.kanade.tachiyomi.util.CrashLogUtil +import eu.kanade.tachiyomi.util.compose.LocalDialogHostState +import eu.kanade.tachiyomi.util.compose.currentOrThrow +import eu.kanade.tachiyomi.util.lang.toTimestampString +import eu.kanade.tachiyomi.util.showNotificationPermissionPrompt +import eu.kanade.tachiyomi.util.system.isOnline +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.localeContext +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withUIContext +import java.text.DateFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import yokai.domain.DialogHostState +import yokai.i18n.MR +import yokai.presentation.component.preference.widget.TextPreferenceWidget +import yokai.presentation.core.components.LinkIcon +import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior +import yokai.presentation.core.icons.CustomIcons +import yokai.presentation.core.icons.Discord +import yokai.presentation.core.icons.GitHub +import yokai.presentation.settings.SettingsScaffold +import yokai.util.Screen +import yokai.util.lang.getString + +class AboutScreen : Screen() { + @Composable + override fun Content() { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + val dialogHostState = LocalDialogHostState.currentOrThrow + val uriHandler = LocalUriHandler.current + + val preferences = remember { Injekt.get() } + + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val listState = rememberLazyListState() + + val requestNotificationPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (!isGranted) { + scope.launch { dialogHostState.awaitNotificationPermissionDeniedDialog() } + } + } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + // FIXME: Move this to MainActivity once the app is fully migrated to Compose + scope.launchIO { + context.checkVersion( + dialogState = dialogHostState, + isUserPrompt = false, + notificationPrompt = { + context.showNotificationPermissionPrompt( + requestNotificationPermission, + false, + preferences, + ) + } + ) + } + } + + val dateFormat by lazy { preferences.dateFormatRaw().get().asDateFormat() } + + SettingsScaffold( + title = stringResource(MR.strings.about), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + appBarScrollBehavior = enterAlwaysCollapsedScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { listState.canScrollForward || listState.canScrollBackward }, + isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }, + ), + content = { contentPadding -> + LazyColumn( + contentPadding = contentPadding, + state = listState, + ) { + item { + TextPreferenceWidget( + title = stringResource(MR.strings.whats_new_this_release), + onPreferenceClick = { + uriHandler.openUri(if (BuildConfig.DEBUG) SOURCE_URL else RELEASE_URL) + }, + ) + } + + if (BuildConfig.INCLUDE_UPDATER) { + item { + TextPreferenceWidget( + title = stringResource(MR.strings.check_for_updates), + onPreferenceClick = { + if (context.isOnline()) { + scope.launch { + context.checkVersion(dialogHostState, true) + } + } else { + context.toast(MR.strings.no_network_connection) + } + }, + ) + } + } + + item { + TextPreferenceWidget( + title = stringResource(MR.strings.version), + subtitle = getVersionName(), + onPreferenceClick = { + val deviceInfo = CrashLogUtil(context.localeContext).getDebugInfo() + val clipboard = context.getSystemService()!! + val appInfo = context.getString(MR.strings.app_info) + clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo)) + scope.launch { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + snackbarHostState.showSnackbar( + message = context.getString(MR.strings._copied_to_clipboard, appInfo), + ) + } + } + }, + ) + } + + item { + TextPreferenceWidget( + title = stringResource(MR.strings.build_time), + subtitle = getFormattedBuildTime(dateFormat), + ) + } + + item { + Column(modifier = Modifier.fillMaxWidth()) { + HorizontalDivider() + + TextPreferenceWidget( + title = stringResource(MR.strings.help_translate), + onPreferenceClick = { uriHandler.openUri("https://hosted.weblate.org/engage/yokai/") }, + ) + } + } + + item { + TextPreferenceWidget( + title = stringResource(MR.strings.helpful_translation_links), + onPreferenceClick = { uriHandler.openUri("https://mihon.app/docs/contribute#helpful-links") }, + ) + } + + item { + TextPreferenceWidget( + title = stringResource(MR.strings.open_source_licenses), + onPreferenceClick = { navigator.push(AboutLicenseScreen()) }, + ) + } + + item { + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + LinkIcon( + label = "Website", + icon = Icons.Outlined.Public, + url = "https://mihon.app", + ) + LinkIcon( + label = "Discord", + icon = CustomIcons.Discord, + url = "https://discord.gg/mihon", + ) + LinkIcon( + label = "GitHub", + icon = CustomIcons.GitHub, + url = "https://github.com/null2264/yokai", + ) + } + } + } + }, + ) + } + + private fun getVersionName(): String = when { + BuildConfig.DEBUG -> "Debug ${BuildConfig.COMMIT_SHA}" + BuildConfig.NIGHTLY -> "Nightly ${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA})" + else -> "Release ${BuildConfig.VERSION_NAME}" + } + + private fun Context.toastIfNotUserPrompt(message: StringResource, isUserPrompt: Boolean) { + toastIfNotUserPrompt(getString(message), isUserPrompt) + } + + private fun Context.toastIfNotUserPrompt(message: String?, isUserPrompt: Boolean) { + if (!isUserPrompt) return + toast(message) + } + + private suspend fun Context.checkVersion(dialogState: DialogHostState, isUserPrompt: Boolean, notificationPrompt: () -> Unit = {}) { + val updateChecker = AppUpdateChecker() + + withUIContext { toastIfNotUserPrompt(MR.strings.searching_for_updates, isUserPrompt) } + + val result = try { + updateChecker.checkForUpdate(this, isUserPrompt) + } catch (error: Exception) { + withUIContext { + toastIfNotUserPrompt(error.message, isUserPrompt) + Logger.e(error) { "Couldn't check new update" } + } + } + when (result) { + is AppUpdateResult.NewUpdate -> { + val data = NewUpdateData( + result.release.info, + result.release.downloadLink, + result.release.preRelease == true + ) + + // Create confirmation window + withUIContext { + if (!isUserPrompt) { notificationPrompt() } + AppUpdateNotifier.releasePageUrl = result.release.releaseLink + dialogState.awaitNewUpdateDialog(data) + } + } + is AppUpdateResult.NoNewUpdate -> { + withUIContext { toastIfNotUserPrompt(MR.strings.no_new_updates_available, isUserPrompt) } + } + } + } +} + +fun getFormattedBuildTime(dateFormat: DateFormat): String { + try { + val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault()) + inputDf.timeZone = TimeZone.getTimeZone("UTC") + val buildTime = + inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME + + return buildTime.toTimestampString(dateFormat) + } catch (e: ParseException) { + return BuildConfig.BUILD_TIME + } +} + +private const val SOURCE_URL = "https://github.com/null2264/yokai/commits/master" diff --git a/app/src/main/java/yokai/presentation/settings/screen/data/AlertDialogs.kt b/app/src/main/java/yokai/presentation/settings/screen/data/AlertDialogs.kt index 29b62334a6..809fff60fb 100644 --- a/app/src/main/java/yokai/presentation/settings/screen/data/AlertDialogs.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/data/AlertDialogs.kt @@ -8,10 +8,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.hippo.unifile.UniFile @@ -22,17 +20,16 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.util.system.toast +import yokai.domain.DialogHostState import yokai.i18n.MR import yokai.presentation.component.LabeledCheckbox import android.R as AR -@Composable -fun RestoreBackup( +suspend fun DialogHostState.awaitRestoreBackup( context: Context, uri: Uri, pair: Pair, - onDismissRequest: () -> Unit, -) { +): Unit = dialog { cont -> val (results, e) = pair if (results != null) { var message = stringResource(MR.strings.restore_content_full) @@ -52,20 +49,20 @@ fun RestoreBackup( } AlertDialog( - onDismissRequest = onDismissRequest, + onDismissRequest = { cont.cancel() }, confirmButton = { TextButton( onClick = { context.toast(MR.strings.restoring_backup) BackupRestoreJob.start(context, uri) - onDismissRequest() + cont.cancel() }, ) { Text(text = stringResource(MR.strings.restore)) } }, dismissButton = { - TextButton(onClick = onDismissRequest) { + TextButton(onClick = { cont.cancel() }) { Text(text = stringResource(AR.string.cancel)) } }, @@ -74,9 +71,9 @@ fun RestoreBackup( ) } else { AlertDialog( - onDismissRequest = onDismissRequest, + onDismissRequest = { cont.cancel() }, confirmButton = { - TextButton(onClick = onDismissRequest) { + TextButton(onClick = { cont.cancel() }) { Text(text = stringResource(AR.string.cancel)) } }, @@ -86,29 +83,27 @@ fun RestoreBackup( } } -@Composable -fun CreateBackup( +suspend fun DialogHostState.awaitCreateBackup( context: Context, uri: Uri, - onDismissRequest: () -> Unit, -) { - var options by remember { mutableStateOf(BackupOptions()) } +): Unit = dialog { cont -> + var options by mutableStateOf(BackupOptions()) AlertDialog( - onDismissRequest = onDismissRequest, + onDismissRequest = { cont.cancel() }, confirmButton = { TextButton(onClick = { val actualUri = UniFile.fromUri(context, uri)?.createFile(Backup.getBackupFilename())?.uri ?: return@TextButton context.toast(MR.strings.creating_backup) BackupCreatorJob.startNow(context, actualUri, options) - onDismissRequest() + cont.cancel() }) { Text(stringResource(MR.strings.create)) } }, dismissButton = { - TextButton(onClick = { onDismissRequest() }) { + TextButton(onClick = { cont.cancel() }) { Text(stringResource(MR.strings.cancel)) } }, diff --git a/app/src/main/java/yokai/presentation/settings/screen/data/StorageInfo.kt b/app/src/main/java/yokai/presentation/settings/screen/data/StorageInfo.kt index 827764ee5f..064a555b20 100644 --- a/app/src/main/java/yokai/presentation/settings/screen/data/StorageInfo.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/data/StorageInfo.kt @@ -18,9 +18,9 @@ import yokai.i18n.MR import yokai.util.lang.getString import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.util.storage.DiskUtil -import yokai.presentation.core.util.secondaryItemAlpha import yokai.presentation.theme.Size import yokai.presentation.theme.header +import yokai.util.secondaryItemAlpha import java.io.File @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/app/src/main/java/yokai/util/EpubReader.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt rename to app/src/main/java/yokai/util/EpubReader.kt index 54c9a3ebd6..2c0869aff4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/app/src/main/java/yokai/util/EpubReader.kt @@ -1,15 +1,16 @@ -package eu.kanade.tachiyomi.util.storage +package yokai.util import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import java.text.ParseException import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale +import yokai.core.archive.EpubReader /** * Fills manga and chapter metadata using this epub file's metadata. */ -fun EpubFile.fillMetadata(chapter: SChapter, manga: SManga) { +fun EpubReader.fillMetadata(chapter: SChapter, manga: SManga) { val ref = getPackageHref() val doc = getPackageDocument(ref) diff --git a/app/src/main/java/yokai/presentation/core/util/ModifierExtensions.kt b/app/src/main/java/yokai/util/ModifierExtensions.kt similarity index 94% rename from app/src/main/java/yokai/presentation/core/util/ModifierExtensions.kt rename to app/src/main/java/yokai/util/ModifierExtensions.kt index 86116f4db1..491c77bae3 100644 --- a/app/src/main/java/yokai/presentation/core/util/ModifierExtensions.kt +++ b/app/src/main/java/yokai/util/ModifierExtensions.kt @@ -1,4 +1,4 @@ -package yokai.presentation.core.util +package yokai.util import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt b/app/src/main/java/yokai/util/coil/ImageViewExtensions.kt similarity index 97% rename from app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt rename to app/src/main/java/yokai/util/coil/ImageViewExtensions.kt index 7f1f864ae5..40f1350410 100644 --- a/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt +++ b/app/src/main/java/yokai/util/coil/ImageViewExtensions.kt @@ -1,4 +1,4 @@ -package yokai.presentation.core.util.coil +package yokai.util.coil import android.view.View import android.widget.ImageView diff --git a/app/src/main/res/drawable/ic_local_library_24dp.xml b/app/src/main/res/drawable/ic_local_library_24dp.xml deleted file mode 100644 index 17aabbae06..0000000000 --- a/app/src/main/res/drawable/ic_local_library_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_shuffle_24dp.xml b/app/src/main/res/drawable/ic_shuffle_24dp.xml new file mode 100644 index 0000000000..425564c27c --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle_24dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/browse_source_controller.xml b/app/src/main/res/layout/browse_source_controller.xml index 699d8109ca..31e054b8fc 100644 --- a/app/src/main/res/layout/browse_source_controller.xml +++ b/app/src/main/res/layout/browse_source_controller.xml @@ -25,11 +25,12 @@ tools:visibility="visible"/> - + + + + + + + + + + diff --git a/app/src/main/res/layout/library_controller.xml b/app/src/main/res/layout/library_controller.xml index 488491a971..59b7870d12 100644 --- a/app/src/main/res/layout/library_controller.xml +++ b/app/src/main/res/layout/library_controller.xml @@ -15,6 +15,12 @@ android:layout_height="75dp" android:layout_gravity="center" /> + + - @@ -69,6 +65,25 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintWidth_min="120dp" /> + + + + + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b015ebe48a..0eb5a7108a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,6 +8,7 @@ @@ -395,4 +396,4 @@ 13sp - \ No newline at end of file + diff --git a/build.gradle.kts b/build.gradle.kts index b85331dcda..d5c90cc3b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,12 +8,6 @@ import java.util.* plugins { alias(libs.plugins.kotlinter) alias(libs.plugins.gradle.versions) - alias(androidx.plugins.application) apply false - alias(androidx.plugins.library) apply false - alias(kotlinx.plugins.android) apply false - alias(kotlinx.plugins.compose.compiler) apply false - alias(kotlinx.plugins.multiplatform) apply false - alias(kotlinx.plugins.parcelize) apply false alias(kotlinx.plugins.serialization) apply false alias(libs.plugins.aboutlibraries) apply false alias(libs.plugins.firebase.crashlytics) apply false @@ -22,46 +16,6 @@ plugins { alias(libs.plugins.sqldelight) apply false } -subprojects { - tasks.withType { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - } - } - - tasks.withType { - useJUnitPlatform() - testLogging { - events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - } - } - - plugins.withType { - configure { - compileSdkVersion(AndroidConfig.compileSdk) - ndkVersion = AndroidConfig.ndk - - defaultConfig { - minSdk = AndroidConfig.minSdk - targetSdk = AndroidConfig.targetSdk - ndk { - version = AndroidConfig.ndk - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - isCoreLibraryDesugaringEnabled = true - } - - dependencies { - add("coreLibraryDesugaring", libs.desugar) - } - } - } -} - tasks.named("dependencyUpdates", com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask::class.java).configure { rejectVersionIf { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { candidate.version.uppercase(Locale.ROOT).contains(it) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 88cd14f786..b825b41720 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,6 +1,21 @@ plugins { `kotlin-dsl` } + +dependencies { + implementation(androidx.gradle) + implementation(kotlinx.gradle) + implementation(kotlinx.compose.compiler.gradle) + implementation(gradleApi()) + + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(files(androidx.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(files(compose.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(files(kotlinx.javaClass.superclass.protectionDomain.codeSource.location)) +} + repositories { + gradlePluginPortal() mavenCentral() -} \ No newline at end of file + google() +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 5b387ffc76..df8c5a31ee 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1 +1,18 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + create("androidx") { + from(files("../gradle/androidx.versions.toml")) + } + create("compose") { + from(files("../gradle/compose.versions.toml")) + } + create("kotlinx") { + from(files("../gradle/kotlinx.versions.toml")) + } + } +} + rootProject.name = "yokai-buildSrc" diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index 742ae66247..025e0fd522 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,6 +1,9 @@ +import org.gradle.api.JavaVersion as GradleJavaVersion + object AndroidConfig { - const val compileSdk = 35 - const val minSdk = 23 - const val targetSdk = 35 - const val ndk = "27.2.12479018" + const val COMPILE_SDK = 35 + const val MIN_SDK = 23 + const val TARGET_SDK = 35 + const val NDK = "27.2.12479018" + val JavaVersion = GradleJavaVersion.VERSION_17 } diff --git a/buildSrc/src/main/kotlin/yokai.android.application.compose.gradle.kts b/buildSrc/src/main/kotlin/yokai.android.application.compose.gradle.kts new file mode 100644 index 0000000000..3bb076cfe8 --- /dev/null +++ b/buildSrc/src/main/kotlin/yokai.android.application.compose.gradle.kts @@ -0,0 +1,10 @@ +import yokai.build.configureCompose + +plugins { + id("com.android.application") + kotlin("android") +} + +android { + configureCompose(this) +} diff --git a/buildSrc/src/main/kotlin/yokai.android.application.gradle.kts b/buildSrc/src/main/kotlin/yokai.android.application.gradle.kts new file mode 100644 index 0000000000..70f34fcad5 --- /dev/null +++ b/buildSrc/src/main/kotlin/yokai.android.application.gradle.kts @@ -0,0 +1,15 @@ +import yokai.build.configureAndroid +import yokai.build.configureTest + +plugins { + id("com.android.application") + kotlin("android") +} + +android { + defaultConfig { + targetSdk = AndroidConfig.TARGET_SDK + } + configureAndroid(this) + configureTest() +} diff --git a/buildSrc/src/main/kotlin/yokai.android.library.compose.gradle.kts b/buildSrc/src/main/kotlin/yokai.android.library.compose.gradle.kts new file mode 100644 index 0000000000..fed5d565de --- /dev/null +++ b/buildSrc/src/main/kotlin/yokai.android.library.compose.gradle.kts @@ -0,0 +1,9 @@ +import yokai.build.configureCompose + +plugins { + id("com.android.library") +} + +android { + configureCompose(this) +} diff --git a/buildSrc/src/main/kotlin/yokai.android.library.gradle.kts b/buildSrc/src/main/kotlin/yokai.android.library.gradle.kts new file mode 100644 index 0000000000..7dd7d66697 --- /dev/null +++ b/buildSrc/src/main/kotlin/yokai.android.library.gradle.kts @@ -0,0 +1,11 @@ +import yokai.build.configureAndroid +import yokai.build.configureTest + +plugins { + id("com.android.library") +} + +android { + configureAndroid(this) + configureTest() +} diff --git a/buildSrc/src/main/kotlin/yokai/build/ProjectExtensions.kt b/buildSrc/src/main/kotlin/yokai/build/ProjectExtensions.kt index df362eaefc..cee0c412ac 100644 --- a/buildSrc/src/main/kotlin/yokai/build/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/yokai/build/ProjectExtensions.kt @@ -1,6 +1,98 @@ package yokai.build +import com.android.build.api.dsl.CommonExtension +import org.gradle.accessors.dm.LibrariesForAndroidx +import org.gradle.accessors.dm.LibrariesForCompose +import org.gradle.accessors.dm.LibrariesForKotlinx +import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.the +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.File +val Project.androidx get() = the() +val Project.compose get() = the() +val Project.kotlinx get() = the() +val Project.libs get() = the() + val Project.generatedBuildDir: File get() = project.layout.buildDirectory.asFile.get().resolve("generated/yokai") + +internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { + commonExtension.apply { + compileSdk = AndroidConfig.COMPILE_SDK + defaultConfig { + minSdk = AndroidConfig.MIN_SDK + ndk { + version = AndroidConfig.NDK + } + } + compileOptions { + sourceCompatibility = AndroidConfig.JavaVersion + targetCompatibility = AndroidConfig.JavaVersion + isCoreLibraryDesugaringEnabled = true + } + } + tasks.withType().configureEach { + kotlinOptions { + jvmTarget = AndroidConfig.JavaVersion.toString() + // freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + // freeCompilerArgs += "-Xcontext-receivers" + // Treat all Kotlin warnings as errors (disabled by default) + // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties + // val warningsAsErrors: String? by project + // allWarningsAsErrors = warningsAsErrors.toBoolean() + } + } + dependencies { + "coreLibraryDesugaring"(libs.desugar) + } +} + +internal fun Project.configureCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) { + pluginManager.apply(kotlinx.plugins.compose.compiler.get().pluginId) + + commonExtension.apply { + buildFeatures { + compose = true + } + + dependencies { + "implementation"(platform(compose.bom)) + } + } + + extensions.configure { + featureFlags.set(setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups)) + + val enableMetrics = project.providers.gradleProperty("enableComposeCompilerMetrics").orNull.toBoolean() + val enableReports = project.providers.gradleProperty("enableComposeCompilerReports").orNull.toBoolean() + + val rootBuildDir = rootProject.layout.buildDirectory.asFile.get() + val relativePath = projectDir.relativeTo(rootDir) + + if (enableMetrics) { + rootBuildDir.resolve("compose-metrics").resolve(relativePath).let(metricsDestination::set) + } + + if (enableReports) { + rootBuildDir.resolve("compose-reports").resolve(relativePath).let(reportsDestination::set) + } + } +} + +internal fun Project.configureTest() { + tasks.withType { + useJUnitPlatform() + testLogging { + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + } +} diff --git a/core/archive/build.gradle.kts b/core/archive/build.gradle.kts new file mode 100644 index 0000000000..e44e620ed5 --- /dev/null +++ b/core/archive/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("yokai.android.library") + kotlin("android") + alias(kotlinx.plugins.serialization) +} + +android { + namespace = "yokai.core.archive" +} + +dependencies { + implementation(libs.jsoup) + implementation(libs.libarchive) + implementation(libs.unifile) +} diff --git a/core/src/androidMain/AndroidManifest.xml b/core/archive/src/main/AndroidManifest.xml similarity index 100% rename from core/src/androidMain/AndroidManifest.xml rename to core/archive/src/main/AndroidManifest.xml diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveEntry.kt b/core/archive/src/main/java/yokai/core/archive/ArchiveEntry.kt similarity index 100% rename from core/src/androidMain/kotlin/yokai/core/archive/ArchiveEntry.kt rename to core/archive/src/main/java/yokai/core/archive/ArchiveEntry.kt diff --git a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt b/core/archive/src/main/java/yokai/core/archive/ArchiveInputStream.kt similarity index 94% rename from core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt rename to core/archive/src/main/java/yokai/core/archive/ArchiveInputStream.kt index fb5426f46a..7c4d7843d3 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt +++ b/core/archive/src/main/java/yokai/core/archive/ArchiveInputStream.kt @@ -1,13 +1,15 @@ package yokai.core.archive +import java.io.InputStream +import java.nio.ByteBuffer +import kotlin.concurrent.Volatile import me.zhanghai.android.libarchive.Archive import me.zhanghai.android.libarchive.ArchiveEntry import me.zhanghai.android.libarchive.ArchiveException -import java.nio.ByteBuffer -import kotlin.concurrent.Volatile -class AndroidArchiveInputStream(buffer: Long, size: Long) : ArchiveInputStream() { +internal class ArchiveInputStream(buffer: Long, size: Long) : InputStream() { private val lock = Any() + @Volatile private var isClosed = false diff --git a/core/archive/src/main/java/yokai/core/archive/ArchiveReader.kt b/core/archive/src/main/java/yokai/core/archive/ArchiveReader.kt new file mode 100644 index 0000000000..d9a4a42298 --- /dev/null +++ b/core/archive/src/main/java/yokai/core/archive/ArchiveReader.kt @@ -0,0 +1,38 @@ +package yokai.core.archive + +import android.os.ParcelFileDescriptor +import android.system.Os +import android.system.OsConstants +import java.io.Closeable +import java.io.InputStream +import me.zhanghai.android.libarchive.ArchiveException + +class ArchiveReader(pfd: ParcelFileDescriptor) : Closeable { + private val size = pfd.statSize + private val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) + + fun useEntries(block: (Sequence) -> T): T = ArchiveInputStream(address, size).use { + block(generateSequence { it.getNextEntry() }) + } + + fun getInputStream(entryName: String): InputStream? { + val archive = ArchiveInputStream(address, size) + try { + while (true) { + val entry = archive.getNextEntry() ?: break + if (entry.name == entryName) { + return archive + } + } + } catch (e: ArchiveException) { + archive.close() + throw e + } + archive.close() + return null + } + + override fun close() { + Os.munmap(address, size) + } +} diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/archive/src/main/java/yokai/core/archive/EpubReader.kt similarity index 96% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt rename to core/archive/src/main/java/yokai/core/archive/EpubReader.kt index b1a2711749..89ba98096b 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/archive/src/main/java/yokai/core/archive/EpubReader.kt @@ -1,16 +1,15 @@ -package eu.kanade.tachiyomi.util.storage +package yokai.core.archive import java.io.Closeable import java.io.File import java.io.InputStream import org.jsoup.Jsoup import org.jsoup.nodes.Document -import yokai.core.archive.ArchiveReader /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(private val reader: ArchiveReader) : Closeable by reader { +class EpubReader(private val reader: ArchiveReader) : Closeable by reader { /** * Path separator used by this epub. diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ZipWriter.kt b/core/archive/src/main/java/yokai/core/archive/ZipWriter.kt similarity index 89% rename from core/src/androidMain/kotlin/yokai/core/archive/ZipWriter.kt rename to core/archive/src/main/java/yokai/core/archive/ZipWriter.kt index dd105245d6..9a776e106c 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/ZipWriter.kt +++ b/core/archive/src/main/java/yokai/core/archive/ZipWriter.kt @@ -4,12 +4,12 @@ import android.content.Context import android.system.Os import android.system.StructStat import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.util.system.openFileDescriptor +import java.io.Closeable +import java.nio.ByteBuffer import me.zhanghai.android.libarchive.Archive import me.zhanghai.android.libarchive.ArchiveEntry import me.zhanghai.android.libarchive.ArchiveException -import java.io.Closeable -import java.nio.ByteBuffer +import yokai.core.archive.util.openFileDescriptor class ZipWriter(val context: Context, file: UniFile) : Closeable { private val pfd = file.openFileDescriptor(context, "wt") @@ -65,10 +65,10 @@ private fun StructStat.toArchiveStat() = ArchiveEntry.StructStat().apply { stSize = st_size stBlksize = st_blksize stBlocks = st_blocks - stAtim = timespec(st_atime) - stMtim = timespec(st_mtime) - stCtim = timespec(st_ctime) + stAtim = st_atime.toTimespec() + stMtim = st_mtime.toTimespec() + stCtim = st_ctime.toTimespec() stIno = st_ino } -private fun timespec(tvSec: Long) = ArchiveEntry.StructTimespec().also { it.tvSec = tvSec } +private fun Long.toTimespec() = ArchiveEntry.StructTimespec().also { it.tvSec = this } diff --git a/core/archive/src/main/java/yokai/core/archive/util/UniFileExtensions.kt b/core/archive/src/main/java/yokai/core/archive/util/UniFileExtensions.kt new file mode 100644 index 0000000000..588f8fcc81 --- /dev/null +++ b/core/archive/src/main/java/yokai/core/archive/util/UniFileExtensions.kt @@ -0,0 +1,14 @@ +package yokai.core.archive.util + +import android.content.Context +import android.os.ParcelFileDescriptor +import com.hippo.unifile.UniFile +import yokai.core.archive.ArchiveReader +import yokai.core.archive.EpubReader + +fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor = + context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: ${filePath ?: uri.toString()}") + +fun UniFile.archiveReader(context: Context): ArchiveReader = openFileDescriptor(context, "r").use { ArchiveReader(it) } + +fun UniFile.epubReader(context: Context): EpubReader = EpubReader(archiveReader(context)) diff --git a/core/build.gradle.kts b/core/main/build.gradle.kts similarity index 92% rename from core/build.gradle.kts rename to core/main/build.gradle.kts index e169090b2b..db1374c6ab 100644 --- a/core/build.gradle.kts +++ b/core/main/build.gradle.kts @@ -1,8 +1,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(androidx.plugins.library) - alias(kotlinx.plugins.multiplatform) + id("yokai.android.library") + kotlin("multiplatform") alias(kotlinx.plugins.serialization) } @@ -60,13 +60,12 @@ kotlin { } android { - namespace = "yokai.core" + namespace = "yokai.core.main" } tasks { withType { compilerOptions.freeCompilerArgs.addAll( - "-Xcontext-receivers", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", ) diff --git a/core/main/src/androidMain/AndroidManifest.xml b/core/main/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/core/main/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt similarity index 95% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index d2a4950347..859af0895c 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.network import java.io.IOException import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.Json @@ -18,6 +17,8 @@ import okhttp3.Response import rx.Observable import rx.Producer import rx.Subscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get val jsonMime = "application/json; charset=utf-8".toMediaType() @@ -68,7 +69,6 @@ fun Call.asObservableSuccess(): Observable { } // Based on https://github.com/gildor/kotlin-coroutines-okhttp -@OptIn(ExperimentalCoroutinesApi::class) private suspend fun Call.await(callStack: Array): Response { return suspendCancellableCoroutine { continuation -> val callback = @@ -131,13 +131,11 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre return progressClient.newCall(request) } -context(Json) inline fun Response.parseAs(): T { - return decodeFromJsonResponse(serializer(), this) + return Injekt.get().decodeFromJsonResponse(serializer(), this) } -context(Json) -fun decodeFromJsonResponse( +fun Json.decodeFromJsonResponse( deserializer: DeserializationStrategy, response: Response, ): T { diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index 72248f17b7..996eba684d 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.network +import java.io.IOException import okhttp3.MediaType import okhttp3.ResponseBody import okio.Buffer @@ -7,7 +8,6 @@ import okio.BufferedSource import okio.ForwardingSource import okio.Source import okio.buffer -import java.io.IOException class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt similarity index 97% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt index dedc62fea3..c236a87960 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.network +import java.util.concurrent.TimeUnit.MINUTES import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.Headers @@ -7,7 +8,6 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.RequestBody -import java.util.concurrent.TimeUnit.* private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_HEADERS = Headers.Builder().build() diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt similarity index 98% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt index e059f2b742..301c5259bb 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.network.interceptor import android.os.SystemClock +import java.io.IOException +import java.util.concurrent.TimeUnit import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response -import java.io.IOException -import java.util.concurrent.* /** * An OkHttp interceptor that handles rate limiting. diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt similarity index 98% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index a1307de1f7..1b52525ca0 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -1,12 +1,12 @@ package eu.kanade.tachiyomi.network.interceptor import android.os.SystemClock +import java.io.IOException +import java.util.concurrent.TimeUnit import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response -import java.io.IOException -import java.util.concurrent.* /** * An OkHttp interceptor that handles given url host's rate limiting. diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt index 1de824381b..2124e6ffd2 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.network.interceptor +import java.io.IOException import okhttp3.Interceptor import okhttp3.Response -import java.io.IOException /** * Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt similarity index 97% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index 47018c7443..edfb857203 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -10,13 +10,14 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast +import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import yokai.i18n.MR -import java.util.* -import java.util.concurrent.* abstract class WebViewInterceptor( private val context: Context, diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt similarity index 98% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt index 21ae2efd98..f02373cf79 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt @@ -67,9 +67,14 @@ object DeviceUtil { val invalidDefaultBrowsers = listOf( "android", + // Honor "com.hihonor.android.internal.app", + // Huawei "com.huawei.android.internal.app", + // Lenovo "com.zui.resolver", + // Infinix + "com.transsion.resolver", ) @SuppressLint("PrivateApi") diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt similarity index 94% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt index 2b674180e0..d2eccb8e30 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt @@ -5,7 +5,6 @@ import android.widget.Toast import androidx.annotation.StringRes import dev.icerock.moko.resources.StringResource import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource /** * Display a toast in this context. diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt similarity index 82% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt index 4c8cc9975c..1e55656c32 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt +++ b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/UniFileExtensions.kt @@ -3,11 +3,9 @@ package eu.kanade.tachiyomi.util.system import android.content.Context import android.os.Build import android.os.FileUtils -import android.os.ParcelFileDescriptor import com.hippo.unifile.UniFile import java.io.BufferedOutputStream import java.io.File -import java.nio.channels.SeekableByteChannel val UniFile.nameWithoutExtension: String? get() = name?.substringBeforeLast('.') @@ -49,6 +47,3 @@ fun UniFile.writeText(string: String, onComplete: () -> Unit = {}) { onComplete() } } - -fun UniFile.openFileDescriptor(context: Context, mode: String): ParcelFileDescriptor = - context.contentResolver.openFileDescriptor(uri, mode) ?: error("Failed to open file descriptor: $displayablePath") diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt similarity index 100% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt rename to core/main/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt diff --git a/core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt b/core/main/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt similarity index 100% rename from core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt rename to core/main/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt index a4909a77ba..16a3dade17 100644 --- a/core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt +++ b/core/main/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt @@ -1,5 +1,8 @@ package yokai.util.lang +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.DelicateCoroutinesApi @@ -12,9 +15,6 @@ import rx.Emitter import rx.Observable import rx.Subscriber import rx.Subscription -import kotlin.coroutines.cancellation.CancellationException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /* * Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY. diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt similarity index 100% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt similarity index 100% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt similarity index 100% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt similarity index 76% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index 99078e89d9..63a8aa1d1d 100644 --- a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -14,6 +14,6 @@ class NetworkPreferences( fun defaultUserAgent() = preferenceStore.getString("default_user_agent", DEFAULT_USER_AGENT) companion object { - const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0" + const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36" } } diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt similarity index 100% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt similarity index 98% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt index d4989d31e9..4bb5215d2f 100644 --- a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt +++ b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt b/core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt similarity index 100% rename from core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt rename to core/main/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt diff --git a/core/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreference.kt b/core/main/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreference.kt similarity index 100% rename from core/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreference.kt rename to core/main/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreference.kt diff --git a/core/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreferenceStore.kt b/core/main/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreferenceStore.kt similarity index 100% rename from core/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreferenceStore.kt rename to core/main/src/iosMain/kotlin/eu/kanade/tachiyomi/core/preference/DarwinPreferenceStore.kt diff --git a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt b/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt deleted file mode 100644 index 8309e9b1a6..0000000000 --- a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt +++ /dev/null @@ -1,42 +0,0 @@ -package yokai.core.archive - -import android.content.Context -import android.os.ParcelFileDescriptor -import android.system.Os -import android.system.OsConstants -import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.util.system.openFileDescriptor -import me.zhanghai.android.libarchive.ArchiveException -import java.io.InputStream - -class AndroidArchiveReader(pfd: ParcelFileDescriptor) : ArchiveReader { - val size = pfd.statSize - val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) - - override fun useEntries(block: (Sequence) -> T): T = - AndroidArchiveInputStream(address, size).use { block(generateSequence { it.getNextEntry() }) } - - override fun getInputStream(entryName: String): InputStream? { - val archive = AndroidArchiveInputStream(address, size) - try { - while (true) { - val entry = archive.getNextEntry() ?: break - if (entry.name == entryName) { - return archive - } - } - } catch (e: ArchiveException) { - archive.close() - throw e - } - archive.close() - return null - } - - override fun close() { - Os.munmap(address, size) - } -} - -fun UniFile.archiveReader(context: Context): ArchiveReader = - openFileDescriptor(context, "r").use { AndroidArchiveReader(it) } diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt deleted file mode 100644 index 6a9cd0185b..0000000000 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt +++ /dev/null @@ -1,6 +0,0 @@ -package yokai.core.archive - -import java.io.InputStream - -// TODO: Use Okio's Source -abstract class ArchiveInputStream : InputStream() diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt deleted file mode 100644 index 00d646f3a7..0000000000 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt +++ /dev/null @@ -1,9 +0,0 @@ -package yokai.core.archive - -import java.io.Closeable -import java.io.InputStream - -interface ArchiveReader : Closeable { - fun useEntries(block: (Sequence) -> T): T - fun getInputStream(entryName: String): InputStream? -} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index e4d4542e3d..4d826d7242 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - alias(kotlinx.plugins.multiplatform) + id("yokai.android.library") + kotlin("multiplatform") alias(kotlinx.plugins.serialization) - alias(androidx.plugins.library) alias(libs.plugins.sqldelight) } @@ -10,6 +10,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(projects.domain) api(libs.bundles.db) } } diff --git a/data/src/commonMain/kotlin/yokai/data/source/browse/filter/SavedSearchRepositoryImpl.kt b/data/src/commonMain/kotlin/yokai/data/source/browse/filter/SavedSearchRepositoryImpl.kt new file mode 100644 index 0000000000..84696c3ad7 --- /dev/null +++ b/data/src/commonMain/kotlin/yokai/data/source/browse/filter/SavedSearchRepositoryImpl.kt @@ -0,0 +1,37 @@ +package yokai.data.source.browse.filter + +import yokai.data.DatabaseHandler +import yokai.domain.source.browse.filter.SavedSearchRepository +import yokai.domain.source.browse.filter.models.RawSavedSearch + +class SavedSearchRepositoryImpl(private val handler: DatabaseHandler) : SavedSearchRepository { + override suspend fun findAll(): List = handler.awaitList { + saved_searchQueries.findAll(RawSavedSearch::mapper) + } + + override fun subscribeAllBySourceId(sourceId: Long) = handler.subscribeToList { + saved_searchQueries.findBySourceId(sourceId, RawSavedSearch::mapper) + } + + override suspend fun findAllBySourceId(sourceId: Long) = handler.awaitList { + saved_searchQueries.findBySourceId(sourceId, RawSavedSearch::mapper) + } + + override suspend fun findOneBySourceIdAndName(sourceId: Long, name: String): RawSavedSearch? = handler.awaitFirstOrNull { + saved_searchQueries.findBySourceIdAndName(sourceId, name, RawSavedSearch::mapper) + } + + override suspend fun findById(id: Long): RawSavedSearch? = handler.awaitFirstOrNull { + saved_searchQueries.findById(id, RawSavedSearch::mapper) + } + + override suspend fun deleteById(id: Long) = handler.await { + saved_searchQueries.deleteById(id) + } + + override suspend fun insert(sourceId: Long, name: String, query: String?, filtersJson: String?) = + handler.awaitOneOrNullExecutable(inTransaction = true) { + saved_searchQueries.insert(sourceId, name, query, filtersJson) + saved_searchQueries.selectLastInsertedRowId() + } +} diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/saved_search.sq b/data/src/commonMain/sqldelight/tachiyomi/data/saved_search.sq new file mode 100644 index 0000000000..8a8e21252c --- /dev/null +++ b/data/src/commonMain/sqldelight/tachiyomi/data/saved_search.sq @@ -0,0 +1,35 @@ +CREATE TABLE saved_search( + -- TODO: Migrate the other tables to use 'id' instead of '_id' + id INTEGER NOT NULL PRIMARY KEY, + source_id INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT, + UNIQUE (source_id, name) +); + +findAll: +SELECT * FROM saved_search +ORDER BY name COLLATE NOCASE ASC; + +findBySourceId: +SELECT * FROM saved_search WHERE source_id = :sourceId +ORDER BY name COLLATE NOCASE ASC; + +findBySourceIdAndName: +SELECT * FROM saved_search WHERE source_id = :sourceId AND name = :name +ORDER BY name COLLATE NOCASE ASC; + +findById: +SELECT * FROM saved_search WHERE id = :id +ORDER BY name COLLATE NOCASE ASC; + +deleteById: +DELETE FROM saved_search WHERE id = :id; + +insert: +INSERT INTO saved_search(source_id, name, query, filters_json) +VALUES (:sourceId, :name, :query, :filtersJson); + +selectLastInsertedRowId: +SELECT last_insert_rowid(); diff --git a/data/src/commonMain/sqldelight/tachiyomi/migrations/28.sqm b/data/src/commonMain/sqldelight/tachiyomi/migrations/28.sqm new file mode 100644 index 0000000000..4a1f0540da --- /dev/null +++ b/data/src/commonMain/sqldelight/tachiyomi/migrations/28.sqm @@ -0,0 +1,8 @@ +CREATE TABLE saved_search( + id INTEGER NOT NULL PRIMARY KEY, + source_id INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT, + UNIQUE (source_id, name) +); diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 6c31e4bfb9..e2f0e2da78 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - alias(kotlinx.plugins.multiplatform) + id("yokai.android.library") + kotlin("multiplatform") alias(kotlinx.plugins.serialization) - alias(androidx.plugins.library) } kotlin { diff --git a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt index da8d99f67b..05a84e0a2a 100644 --- a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt +++ b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt @@ -82,9 +82,6 @@ interface Manga : SManga { } } - fun isBlank() = id == Long.MIN_VALUE - fun isHidden() = status == -1 - fun setChapterOrder(sorting: Int, order: Int) { setChapterFlags(sorting, CHAPTER_SORTING_MASK) setChapterFlags(order, CHAPTER_SORT_MASK) diff --git a/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/SavedSearchRepository.kt b/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/SavedSearchRepository.kt new file mode 100644 index 0000000000..abce233d30 --- /dev/null +++ b/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/SavedSearchRepository.kt @@ -0,0 +1,14 @@ +package yokai.domain.source.browse.filter + +import kotlinx.coroutines.flow.Flow +import yokai.domain.source.browse.filter.models.RawSavedSearch + +interface SavedSearchRepository { + suspend fun findAll(): List + fun subscribeAllBySourceId(sourceId: Long): Flow> + suspend fun findAllBySourceId(sourceId: Long): List + suspend fun findOneBySourceIdAndName(sourceId: Long, name: String): RawSavedSearch? + suspend fun findById(id: Long): RawSavedSearch? + suspend fun deleteById(id: Long) + suspend fun insert(sourceId: Long, name: String, query: String?, filtersJson: String?): Long? +} diff --git a/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/models/RawSavedSearch.kt b/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/models/RawSavedSearch.kt new file mode 100644 index 0000000000..b1ca1bdd2a --- /dev/null +++ b/domain/src/commonMain/kotlin/yokai/domain/source/browse/filter/models/RawSavedSearch.kt @@ -0,0 +1,25 @@ +package yokai.domain.source.browse.filter.models + +data class RawSavedSearch( + val id: Long, + val sourceId: Long, + val name: String, + val query: String?, + val filtersJson: String?, +) { + companion object { + fun mapper( + id: Long, + sourceId: Long, + name: String, + query: String?, + filtersJson: String?, + ) = RawSavedSearch( + id = id, + sourceId = sourceId, + name = name, + query = query, + filtersJson = filtersJson, + ) + } +} diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index c65c235f27..8ca4a27c21 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,9 +1,11 @@ [versions] activity = "1.9.3" agp = "8.7.3" -lifecycle = "2.8.7" +lifecycle = "2.9.0" [libraries] +gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" } + activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } annotation = { module = "androidx.annotation:annotation", version = "1.9.1" } @@ -11,10 +13,10 @@ appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } browser = { module = "androidx.browser:browser", version = "1.8.0" } biometric = { module = "androidx.biometric:biometric", version = "1.1.0" } cardview = { module = "androidx.cardview:cardview", version = "1.0.0" } -core = { module = "androidx.core:core-ktx", version = "1.15.0" } +core = { module = "androidx.core:core-ktx", version = "1.16.0" } core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } glance-appwidget = { module = "androidx.glance:glance-appwidget", version = "1.1.1" } -layout-constraint = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.0" } +layout-constraint = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" } layout-swiperefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" } lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" } lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } @@ -25,11 +27,11 @@ lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel multidex = { module = "androidx.multidex:multidex", version = "2.0.1" } palette = { module = "androidx.palette:palette", version = "1.0.0" } preference = { module = "androidx.preference:preference-ktx", version = "1.2.1" } -recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.3.2" } -sqlite = { module = "androidx.sqlite:sqlite", version = "2.4.0" } -webkit = { module = "androidx.webkit:webkit", version = "1.12.0" } -work = { module = "androidx.work:work-runtime-ktx", version = "2.10.0" } -window = { module = "androidx.window:window", version = "1.3.0" } +recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.4.0" } +sqlite = { module = "androidx.sqlite:sqlite", version = "2.5.1" } +webkit = { module = "androidx.webkit:webkit", version = "1.13.0" } +work = { module = "androidx.work:work-runtime-ktx", version = "2.10.1" } +window = { module = "androidx.window:window", version = "1.4.0" } [bundles] androidx = [ diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index eb7c047db8..86eb7c364f 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,12 +1,12 @@ [versions] -compose = "2024.12.01" +compose = "2025.05.01" [libraries] bom = { module = "androidx.compose:compose-bom", version.ref = "compose" } animation = { module = "androidx.compose.animation:animation" } foundation = { module = "androidx.compose.foundation:foundation" } material3 = { module = "androidx.compose.material3:material3" } -material-motion = { module = "io.github.fornewid:material-motion-compose-core", version = "1.0.7" } +material-motion = { module = "io.github.fornewid:material-motion-compose-core", version = "1.2.1" } ui-tooling = { module = "androidx.compose.ui:ui-tooling" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } icons = { module = "androidx.compose.material:material-icons-extended" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 2384d6c058..b7fe860fd0 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,10 +1,13 @@ [versions] -kotlin = "2.1.0" -serialization = "1.7.3" -xml_serialization = "0.90.3" +kotlin = "2.1.21" +serialization = "1.8.1" +xml_serialization = "0.91.1" [libraries] -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.9.0" } +gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } + +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.10.2" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } @@ -12,8 +15,8 @@ serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" } serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" } serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xml_serialization" } -serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-android", version.ref = "xml_serialization" } -immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.8" } +serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xml_serialization" } +immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" } [bundles] serialization = [ @@ -26,4 +29,4 @@ android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d3fc9ec4e..1edaecae2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] -aboutlibraries = "11.2.3" +aboutlibraries = "11.6.3" chucker = "3.5.2" flexible-adapter = "c8013533" fast_adapter = "5.7.0" -moko = "0.24.4" -okhttp = "5.0.0-alpha.14" +moko = "0.24.5" +okhttp = "5.0.0-alpha.16" shizuku = "13.1.5" # FIXME: Uncomment once SQLDelight support KMP AndroidX SQLiteDriver #sqlite = "2.5.0-alpha04" -sqlite = "2.4.0" +sqlite = "2.5.1" sqldelight = "2.0.2" junit = "5.11.3" kermit = "2.0.5" -koin = "4.0.0" +koin = "4.0.4" leakcanary = "2.14" voyager = "1.1.0-beta03" @@ -21,21 +21,21 @@ aboutlibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.re chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" } chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" } -coil3-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.4" } +coil3-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.2.0" } coil3 = { module = "io.coil-kt.coil3:coil" } coil3-svg = { module = "io.coil-kt.coil3:coil-svg" } coil3-gif = { module = "io.coil-kt.coil3:coil-gif" } coil3-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } -compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" } +compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.36.0" } conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" } conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.2" } -desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.1.3" } +desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.1.5" } directionalviewpager = { module = "com.github.tachiyomiorg:DirectionalViewPager", version = "1.0.0" } disklrucache = { module = "com.jakewharton:disklrucache", version = "2.0.2" } fastadapter-extensions-binding = { module = "com.mikepenz:fastadapter-extensions-binding", version.ref = "fast_adapter" } fastadapter = { module = "com.mikepenz:fastadapter", version.ref = "fast_adapter" } -firebase = { module = "com.google.firebase:firebase-bom", version = "33.7.0" } +firebase = { module = "com.google.firebase:firebase-bom", version = "33.14.0" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" } @@ -54,22 +54,22 @@ kotest-assertions = { module = "io.kotest:kotest-assertions-core", version = "5. leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } -libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.4" } +libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.5" } material = { module = "com.google.android.material:material", version = "1.12.0" } markwon = { module = "io.noties.markwon:core", version = "4.6.2" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version = "v3.1.0" } java-nat-sort = { module = "com.github.gpanther:java-nat-sort", version = "natural-comparator-1.1" } -jsoup = { module = "org.jsoup:jsoup", version = "1.18.3" } +jsoup = { module = "org.jsoup:jsoup", version = "1.20.1" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-android = { module = "androidx.test.ext:junit", version = "1.2.1" } -mockk = { module = "io.mockk:mockk", version = "1.13.13" } +mockk = { module = "io.mockk:mockk", version = "1.14.2" } moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" } moko-resources-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko" } -okio = { module = "com.squareup.okio:okio", version = "3.9.1" } +okio = { module = "com.squareup.okio:okio", version = "3.12.0" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } @@ -85,7 +85,7 @@ slice = { module = "com.github.mthli:Slice", version = "v1.2" } # FIXME: Uncomment once SQLDelight support KMP AndroidX SQLiteDriver #sqlite = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } -sqlite-android = { module = "com.github.requery:sqlite-android", version = "3.45.0" } +sqlite-android = { module = "com.github.requery:sqlite-android", version = "3.49.0" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } @@ -95,7 +95,7 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", subsamplingscaleimageview = { module = "com.github.null2264:subsampling-scale-image-view", version = "f7b674ebdd" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } -taptargetview = { module = "com.getkeepsafe.taptargetview:taptargetview", version = "1.13.3" } +taptargetview = { module = "com.getkeepsafe.taptargetview:taptargetview", version = "1.15.0" } unifile = { module = "com.github.tachiyomiorg:unifile", version = "a9de196cc7" } viewstatepageradapter = { module = "com.nightlynexus.viewstatepageradapter:viewstatepageradapter", version = "1.1.0" } viewtooltip = { module = "com.github.CarlosEsco:ViewTooltip", version = "f79a8955ef" } # FIXME: Don't depends on this @@ -106,10 +106,10 @@ voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", vers [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.3" } google-services = { id = "com.google.gms.google-services", version = "4.4.2" } -gradle-versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } -kotlinter = { id = "org.jmailen.kotlinter", version = "5.0.1" } +gradle-versions = { id = "com.github.ben-manes.versions", version = "0.52.0" } +kotlinter = { id = "org.jmailen.kotlinter", version = "5.1.0" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8200..cea7a793a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/i18n/README.md b/i18n/README.md new file mode 100644 index 0000000000..ae5b94078b --- /dev/null +++ b/i18n/README.md @@ -0,0 +1,5 @@ +# i18n + +This module houses the string resources and translations. + +Original English strings are managed in `src/commonMain/moko-resources/base/`. Translations are done externally via [Weblate](https://hosted.weblate.org/projects/yokai/). diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts index 3879a5b92e..6cf146d185 100644 --- a/i18n/build.gradle.kts +++ b/i18n/build.gradle.kts @@ -2,8 +2,8 @@ import yokai.build.generatedBuildDir import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(kotlinx.plugins.multiplatform) - alias(androidx.plugins.library) + id("yokai.android.library") + kotlin("multiplatform") alias(libs.plugins.moko) } diff --git a/i18n/src/commonMain/moko-resources/ar/plurals.xml b/i18n/src/commonMain/moko-resources/ar/plurals.xml index 2bdf73bcba..ee1cc5e004 100644 --- a/i18n/src/commonMain/moko-resources/ar/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ar/plurals.xml @@ -8,7 +8,6 @@ يتوفَّر %d تحديثًا للإضافات يتوفَّر %d تحديث للإضافات - لا توجد فئات فئة @@ -17,16 +16,6 @@ %d فئةً %d فئة - - - انتهت الصفحات - تبقَّت صفحة - تبقَّت صفحتان - تبقَّت %1$d صفحات - تبقَّت %1$d صفحةً - تبقَّت %1$d صفحة - - لم يُزل أيُّ فصل من المصدر: \n%2$s @@ -47,7 +36,6 @@ \n%2$s \nأأحذف تنزيلاتهم؟ - ألا أزيل أيَّ فصل؟ أأزيل فصلًا منزَّلًا؟ @@ -56,7 +44,6 @@ أأزيل %1$d فصلًا منزَّلًا؟ أأزيل %1$d فصل منزَّل؟ - بعد %1$s دقيقة بعد %1$s دقيقة @@ -65,7 +52,6 @@ بعد %1$s دقائق بعد %1$s دقائق - تم التنظيف. تمت إزالة %d مجلد تم التنظيف. تمت إزالة %d مجلد @@ -74,7 +60,6 @@ تم التنظيف. تمت إزالة %d مجلدات تم التنظيف. تمت إزالة %d مجلدات - لصفر عنوان لعنوان @@ -83,7 +68,6 @@ ل‍ %d عنوانًا ل‍ %d عنوان - ولا أيِّ فصل وفصل @@ -92,7 +76,6 @@ و %1$d فصلًا و %1$d فصل - تمَّ في %1$s وبدون أخطاء تمَّ في %1$s وفيه خطأ @@ -101,7 +84,6 @@ تمَّ في %1$s وفيه %2$s خطأً تمَّ في %1$s وفيه %2$s خطأ - لا يوجد أيُّ فصل فصل واحد @@ -110,7 +92,6 @@ %1$s فصلًا %1$s فصل - لم تُرحَّل أيُّ سلسلة رُحِّلت السلسلة @@ -119,7 +100,6 @@ رُحِّلت %d سلسلةً رُحِّلت %d سلسلةٍ - ألا أنسخ أيَّ سلسلة؟ أأنسخ السلسلة؟ @@ -128,7 +108,6 @@ أأنسخ %1$d (بتخطي %2$s) سلسلةً؟ أأنسخ %1$d (بتخطي %2$s) سلسلةٍ؟ - ألا أرحِّل أيَّ سلسلة؟ أأرحِّل السلسلة؟ @@ -137,7 +116,6 @@ أأرحِّل %1$d (بتخطِّي %2$s) سلسلةً؟ أأرحِّل %1$d (بتخطِّي %2$s) سلسلةٍ؟ - لا توجد صفحات صفحة @@ -146,7 +124,6 @@ %1$d صفحةً %1$d صفحة - لا يوجد تحديث معلَّق يوجد تحديث معلَّق @@ -155,7 +132,6 @@ يوجد %d تحديثًا معلَّقًا يوجد %d تحديث معلَّق - لم تُحدَّث أيُّ إضافة حُدِّثت إضافة @@ -164,7 +140,6 @@ حُدِّثت %d إضافةً حُدِّثت %d إضافة - لا يُتخطَّى أيُّ فصل يُتخطَّى فصل، وذلك إما لأن المصدر مفقود أو لأنه مصفًّى @@ -173,7 +148,6 @@ يُتخطَّى %d فصلًا، وذلك إما لأن المصدر مفقود أو لأنهم مصفَّون يُتخطَّى %d فصل، وذلك إما لأن المصدر مفقود أو لأنهم مصفَّون - تم تنظيف الملفات المؤقتة. تم حذف %d من الملفات تم تنظيف الملفات المؤقتة. تم حذف %d من الملفات @@ -182,7 +156,6 @@ تم تنظيف الملفات المؤقتة. تم حذف %d من الملفات تم تنظيف الملفات المؤقتة. تم حذف %d من الملفات - لا يوجد فصل تالٍ لم يُقرأ الفصل غير المقروء التالي @@ -191,7 +164,6 @@ %d فصلًا تاليًا لم يُقرؤوا %d فصل تالٍ لم يُقرؤوا - %d انواع المسلسلات %d انواع المسلسلات @@ -200,7 +172,6 @@ %d عدة انواع من المسلسلات %d انواع من المسلسلات - %d مصدر %d مصدر @@ -209,7 +180,6 @@ %d مصادر %d مصادر - %d لغة %d لغة @@ -218,7 +188,6 @@ %d لغات %d لغات - %d وضع %d وضع @@ -227,4 +196,4 @@ %d اوضاع %d اوضاع - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ar/strings.xml b/i18n/src/commonMain/moko-resources/ar/strings.xml index dae24365fe..8fbfe8ca83 100644 --- a/i18n/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n/src/commonMain/moko-resources/ar/strings.xml @@ -753,11 +753,6 @@ حسب أرقام الفصول حسب ترتيب المصدر لم يعلَّم - يتطلب TachiyomiJ2K الوصول إلى جميع الملفات لينزِّل الفصول. اضغط هنا ثم مكِّن «السماح بالوصول لإدارة جميع الملفات.» - يحتاج TachiyomiJ2K للوصول إلى كلِّ الملفَّات في أندرويد ١١، فبذلك ينزِّل الفصول ويحتاط ويقرأ السلاسل المنزَّلة. - \n - \nمكِّن «اسمح بإدارة كافة الملفات.» عندما تراها في الشاشة التالية - إذن الوصول إلى الملفات مطلوب تحذير بحث %1$s الاتجاه @@ -904,7 +899,6 @@ بنفسجي ٥٪ سلسلة وكيل المستخدم المبدئية - تنسيق RARv5 ليس مدعومًا تتطلَّب بعض اللغات إعادة التشغيل لتظهر صحيحةً الفصول المنزَّلة التحميل تلقائيا أثناء القراءة @@ -963,4 +957,4 @@ معلومات التنقيح الأنشطة التي تعمل في الخلفية شارك الغلاف - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/az/plurals.xml b/i18n/src/commonMain/moko-resources/az/plurals.xml index 31d3381bad..622df450e9 100644 --- a/i18n/src/commonMain/moko-resources/az/plurals.xml +++ b/i18n/src/commonMain/moko-resources/az/plurals.xml @@ -4,14 +4,8 @@ Endirilmiş %1$d bölüm silinsin\? Endirilmiş %1$d bölüm silinsin\? - %1$s bölüm %1$s bölüm - - - %1$d səhifə qaldı - %1$d səhifə qaldı - - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/az/strings.xml b/i18n/src/commonMain/moko-resources/az/strings.xml index 1558940540..01f3695acd 100644 --- a/i18n/src/commonMain/moko-resources/az/strings.xml +++ b/i18n/src/commonMain/moko-resources/az/strings.xml @@ -2,7 +2,6 @@ Ad Daha çox - TachiyomiJ2K bölümləri yükləmək üçün bütün fayllara giriş tələb edir. Bura klikləyin, sonra \"Bütün faylları idarə etmək üçün girişə icazə verin\" funksiyasını aktivləşdirin. Manga Komiks Davam edir @@ -40,17 +39,12 @@ Sırala Bölümlər tapılmadı Səhifələr tapılmadı - Fayl icazələri tələb olunur - TachiyomiJ2K bölümləri yükləmək, avtomatik ehtiyat nüsxələri yaratmaq və lokal manqa oxumaq üçün Android 11-də bütün fayllara giriş icazəsi olmalıdır. - \n - \nNövbəti ekranda \"Bütün faylları idarə etmək üçün girişə icazə verin\" seçimini aktivləşdirin. Manhwa Manhua Boş olduqda kilidlə Bölümlər Oxundu olaraq işarələndi Bölümlər silindi. - RARv5 formatı dəstəklənmir Bütün endirilənlər silinsin\? Silmək üçün bölüm yoxdur Mənbə sırasıyla @@ -59,4 +53,4 @@ Skanlyatorlar Kateqoriya Kateqoriyalar - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 92b218f85f..3e52a4026a 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -7,10 +7,6 @@ More options Navigate up - File permissions required - TachiyomiJ2K requires access to all files in Android 11 to download chapters, create automatic backups, and read local series. \n\nOn the next screen, enable \"Allow access to manage all files.\" - TachiyomiJ2K requires access to all files to download chapters. Tap here, then enable \"Allow access to manage all files.\" - Welcome! Let\'s pick some defaults. You can always change these things later in the settings. Get started @@ -170,6 +166,12 @@ Tracking status Ungrouped + Use experimental compose library + Behavior + Mark duplicate read chapter as read + After reading a chapter + After fetching new chapter + Sort by Total chapters @@ -178,6 +180,7 @@ Date fetched Latest chapter Drag & Drop + Random Display options @@ -296,7 +299,7 @@ Clear history Show download queue Open last read chapter - Long tap Recents behaviour + Long tap Recents behavior Search filters @@ -319,7 +322,7 @@ Check website in WebView Open extensions / migration menu Open global search - Long tap Browse behaviour + Long tap Browse behavior @@ -500,7 +503,7 @@ Start past cutout Cutout area behavior only applies in portrait mode with certain scale types Open legacy cutout settings - On devices older than Android 9.0, there\'s no way to modify cutout setting other than setting it manually through to your system settings + On devices older than Android 9.0, you need to set cutout settings manually through your system settings Show content in cutout area Ignore cutout areas Page layout @@ -794,8 +797,8 @@ Clear chapter cache - Refresh the download cache - This will force the download cache to recalculate. Useful if you modified downloads outside of this app and want the app to pick them up + Reindex downloads + Force app to recheck downloaded chapters Data Management Check for beta releases @@ -885,6 +888,7 @@ You can also migrate by selecting entries in your library Source migration guide + Enable source swipe action NSFW (18+) sources Show in sources and extensions lists This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app. @@ -1154,6 +1158,12 @@ Default orientation Orientation Save + Saved searches + Save current search query? + Save search name + Invalid saved search name + Delete this saved search query? + Are you sure you wish to delete this saved search query: \'%1$s\'? Search Search %1$s Select all diff --git a/i18n/src/commonMain/moko-resources/bg/plurals.xml b/i18n/src/commonMain/moko-resources/bg/plurals.xml index 92490a53d9..c8dc3ddf74 100644 --- a/i18n/src/commonMain/moko-resources/bg/plurals.xml +++ b/i18n/src/commonMain/moko-resources/bg/plurals.xml @@ -4,77 +4,58 @@ 1 наличен ъпдейт за разширение %d налични ъпдейта за разширения - Направено за %1$s с %2$s грешка Направено за %1$s с %2$s грешки - %d категория %d категории - - - %1$d страница останала - %1$d страници останали - - %1$s глава %1$s глави - Изтри %1$d изтеглената глава\? Изтри %1$d изтеглените глави\? - Има 1 липсваща глава, източникът липса или е филтриран Има %d липсващи глави, източникът липса или е филтриран - Копирайте %1$d%2$s манга\? Копирайте %1$d%2$sманги\? - Разширението е актуализирано %d разширения са актуализирани - %d преместена манга %d преместени манги - Миграция на %1$d%2$s манга\? Миграция на %1$d%2$s манги\? - За %d заглавие За %d заглавия - %d чакаща актуализация %d чакаща актуализации - %1$d страница %1$d страници - и %1$d повече глава и %1$d повече глави - Една глава е премахната от източника: \n%2$s @@ -84,24 +65,20 @@ \n \nИзтриване на изтеглянето им\? - Почистването е завършено. Премахната е папката %d Почистването е завършено. Премахнати са папките %d - След %1$s минута След %1$s минути - Кешът е изчистен. %d файлът е изтрит Кешът е изчистен. %d файловете са изтрити - Следващата непрочетена глава Следващите %d непрочетени глави - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/bg/strings.xml b/i18n/src/commonMain/moko-resources/bg/strings.xml index e656aeb267..a44cce0d55 100644 --- a/i18n/src/commonMain/moko-resources/bg/strings.xml +++ b/i18n/src/commonMain/moko-resources/bg/strings.xml @@ -543,11 +543,7 @@ Бутони за тема, базирани на корицата През всяка мрежа Не актуализирайте автоматично - Необходими са разрешения за файлове Изтрита категория - TachiyomiJ2K изисква достъп до всички файлове в Android 11, за да изтегля глави, да създава автоматични резервни копия и да чете локална манга. - \n - \nНа следващия екран разрешете \"Разрешаване на достъп за управление на всички файлове.\" Добавете категории Няма намерени съвпадения за текущите ви филтри Покажи всички категории @@ -667,7 +663,6 @@ Автоматични актуализации Приложение за автоматично актуализиране Само през Wi-Fi - TachiyomiJ2K изисква достъп до всички файлове, за да изтегли главите. Докоснете тук, след което разрешете \"Разрешаване на достъп за управление на всички файлове.\" Не можа да се инсталира актуализация Завършена актуализация Вашата библиотека @@ -888,7 +883,6 @@ %1$d пропуснати обновление/я Пропусната, понеже не е започната Изчисти данните на WebView - Форматът RARv5 не се поддържа Грешка: празен URI Подобрява производителността на четеца Данните на WebView изчистени @@ -921,4 +915,4 @@ Изключени категории Инсталатор Работа на заден план - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/bn/plurals.xml b/i18n/src/commonMain/moko-resources/bn/plurals.xml index fd06eb40fb..3cdfe28a11 100644 --- a/i18n/src/commonMain/moko-resources/bn/plurals.xml +++ b/i18n/src/commonMain/moko-resources/bn/plurals.xml @@ -4,37 +4,26 @@ এক্সটেনশন আপডেট পাওয়া গেছে %dটি এক্সটেনশন আপডেট পাওয়া গেছে - %d বিভাগ %d বিভাগসমূহ - - - %1$d পৃষ্ঠা বাকি - %1$d গুলি পৃষ্ঠা বাকি - - %1$s সময়ে %2$s ত্রুটি সম্পন্ন %1$s সময়ে %2$sটি ত্রুটি সম্পন্ন - %1$dটি ডাউনলোড করা অধ্যায় সরাবেন\? ডাউনলোড করা %1$dটি অধ্যায় সরাবেন\? - %1$sটি অধ্যায় %1$s গুলি অধ্যায় - 1 অধ্যায় এড়িয়ে জাছি, হয় উৎসটি অনুপস্থিত বা এটি ফিল্টার করা হয়েছে %d অধ্যায় কিপ করা হচ্ছে, হয় উৎসটি তাদের অনুপস্থিত অথবা সেগুলি ফিল্টার করা হয়েছে - উৎস থেকে একটি অধ্যায় সরানো হয়েছে: \n%2$s @@ -44,64 +33,52 @@ \n \nতাদের ডাউনলোড মুছে ফেলবেন\? - %d টি মাঙ্গা স্থানান্তরিত হয়েছে %d টা মাঙ্গা স্থানান্তরিত হয়েছে - %1$d%2$s মাঙ্গা কপি করবেন\? %1$d%2$s টা মাঙ্গা কপি করবেন\? - ক্যাশে সাফ করা হয়েছে। %d ফাইল মুছে ফেলা হয়েছে ক্যাশে সাফ করা হয়েছে। %d টা ফাইল মুছে ফেলা হয়েছে - পরিষ্কার করা হয়েছে। %d ফোল্ডার সরানো হয়েছে পরিষ্কার করা হয়েছে। %d টা ফোল্ডার সরানো হয়েছে - %d শিরোনামের জন্য %d গুল শিরোনামের জন্য - এবং আরো %1$d টি অধ্যায় এবং আরো %1$d গুল অধ্যায় - %d আপডেট মুলতুবি আছে %d গুলো আপডেট মুলতুবি আছে - এক্সটেনশন আপডেট করা হয়েছে %d টি এক্সটেনশন আপডেট করা হয়েছে - %1$dটা পৃষ্ঠা %1$dটি পৃষ্ঠা - %1$d%2$s মাঙ্গা মাইগ্রেট করবেন\? %1$d%2$s টা মাঙ্গা মাইগ্রেট করবেন\? - %1$s মিনিট পরে %1$s টা মিনিট পরে - পরবর্তী অপঠিত অধ্যায় পরবর্তী %d টি অপঠিত অধ্যায় - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/bn/strings.xml b/i18n/src/commonMain/moko-resources/bn/strings.xml index b132c9ec29..da79a8a07f 100644 --- a/i18n/src/commonMain/moko-resources/bn/strings.xml +++ b/i18n/src/commonMain/moko-resources/bn/strings.xml @@ -576,11 +576,6 @@ বিভাগ যোগ করুন/সম্পাদনা করুন %1$s কে এতে সরান… এতে %1$s যোগ করুন… - ফাইল অনুমতি প্রয়োজন - TachiyomiJ2K-এর জন্য অধ্যায়গুলি ডাউনলোড করতে, স্বয়ংক্রিয় ব্যাকআপ তৈরি করতে এবং স্থানীয় মাঙ্গা পড়তে Android 11-এর সমস্ত ফাইল অ্যাক্সেস করতে হবে। -\n -\nপরবর্তী স্ক্রিনে, \"সমস্ত ফাইল পরিচালনা করতে অ্যাক্সেসের অনুমতি দিন\" সক্ষম করুন।\" - TachiyomiJ2K-এর অধ্যায়গুলি ডাউনলোড করার জন্য সমস্ত ফাইলে অ্যাক্সেস প্রয়োজন। এখানে আলতো চাপুন, তারপর \"সমস্ত ফাইল পরিচালনা করতে অ্যাক্সেসের অনুমতি দিন\" সক্ষম করুন৷\" এক্সটেনশন ইনস্টলার হিসাবে Shizuku ব্যবহার করতে Shizuku ইনস্টল করুন এবং শুরু করুন। শিজুকু চলছে না সঙ্কুচিত ডায়নামিক বিভাগগুলিকে নীচে নিয়ে যাও @@ -887,7 +882,6 @@ %1$s দেখান শিরোনামসমুহ হালনাগাদ এড়িয়ে যান অপঠিত অধ্যায়সহ - RARv5 ধরন সমর্থিত নয় অপঠিত অধ্যায় থাকায় এড়িয়ে যাওয়া হয়েছে FAQ এবং নির্দেশনা সিরিজ কে উপরে তুলুন @@ -902,4 +896,4 @@ পুস্তক সংগ্রহ ডিবাগ তথ্য পটভূমি কার্যকলাপ - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ca/plurals.xml b/i18n/src/commonMain/moko-resources/ca/plurals.xml index 31ea29be14..d8403751b6 100644 --- a/i18n/src/commonMain/moko-resources/ca/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ca/plurals.xml @@ -4,52 +4,42 @@ Hi ha una actualització d’una extensió Hi ha actualitzacions de %d extensions - Voleu eliminar %1$d capítol baixat\? Voleu eliminar %1$d capítols baixats\? - %d categoria %d categories - Fet en %1$s amb %2$s error Fet en %1$s amb %2$s errors - %1$s capítol %1$s capítols - S’ha omès %d capítol. És possible que manqui a la font o que hagi estat filtrat S’han omès %d capítols. És possible que manquin a la font o que hagin estat filtrats - Per a %d títol Per a %d títols - i %1$d capítol més i %1$d capítols més - El següent capítol no llegit Els següents %d capítols no llegits - %1$d pàgina %1$d pàgines - S’ha suprimit un capítol d’aquesta font: \n%2$s @@ -59,14 +49,8 @@ \n \nVoleu eliminar-ne les baixades\? - - - %1$d pàgina restant - %1$d pàgines restants - - Després d’%1$s minut Després de %1$s minuts - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ca/strings.xml b/i18n/src/commonMain/moko-resources/ca/strings.xml index 70e4ce6afa..b4441fe170 100644 --- a/i18n/src/commonMain/moko-resources/ca/strings.xml +++ b/i18n/src/commonMain/moko-resources/ca/strings.xml @@ -452,7 +452,6 @@ Categoria Mou %1$s a… Afegeix %1$s a… - Calen permisos de fitxers Categoria suprimida Vés a la categoria Gestiona la categoria @@ -568,20 +567,15 @@ Sense descripció 5% Cadena d’agent d’usuari per defecte - El format RARv5 no està suportat Capítols baixats Baixa automàticament mentre es llegeix Baixa per avançat Només funciona en elements de la biblioteca i si el capítol actual i el següent ja estan baixats - El TachiyomiJ2K requereix l’accés a tots els fitxers per a poder baixar capítols. Premeu aquí i activeu «Permet l’accés a tots els fitxers». No s’ha pogut instal·lar l’actualització Pàgina del llançament La vostra biblioteca Nova versió beta disponible! Actualització completada - El TachiyomiJ2K requereix l’accés a tots els fitxers en Android 11 per a poder baixar capítols, crear còpies de seguretat automàtiques i llegir mangues locals. -\n -\nA la següent pantalla, activeu «Permet l’accés a tots els fitxers». S’ha omès perquè no cal actualitzar la sèrie La cadena d’agent d’usuari no és vàlida Temps de lectura @@ -735,4 +729,4 @@ Configuració de l’aplicació Informació de depuració Activitat en segon pla - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ceb/strings.xml b/i18n/src/commonMain/moko-resources/ceb/strings.xml index e910d89d3e..3f68b3cc73 100644 --- a/i18n/src/commonMain/moko-resources/ceb/strings.xml +++ b/i18n/src/commonMain/moko-resources/ceb/strings.xml @@ -67,11 +67,6 @@ Tuo ug Wala Baliktad Edge - Gikinahanglan ang mga permiso sa file - Ang TachiyomiJ2K nanginahanglan pag-access sa tanan nga mga file sa Android 11 aron ma-download ang mga kapitulo, paghimo og awtomatikong pag-backup, ug pagbasa sa lokal nga manga. -\n -\nSa sunod nga screen, i-enable ang \"Allow access to manage all files.\" - Ang TachiyomiJ2K nanginahanglan og access sa tanan nga mga file aron ma-download ang mga kapitulo. I-tap dinhi, dayon i-enable ang \"Allow access to manage all files.\" Manhwa Tuo ngadto sa wala Bertikal @@ -332,4 +327,4 @@ Installer Mga entri sa basahonan Kalihokan sa background - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/cs/plurals.xml b/i18n/src/commonMain/moko-resources/cs/plurals.xml index eb9035c525..08318c9b2b 100644 --- a/i18n/src/commonMain/moko-resources/cs/plurals.xml +++ b/i18n/src/commonMain/moko-resources/cs/plurals.xml @@ -5,43 +5,31 @@ Dokončeno za %1$s s %2$s chybami Dokončeno za %1$s s %2$s chybami - %d kategorie %d kategorie %d kategorií - - - Zbývá %1$d stránka - Zbývají %1$d stránky - Zbývá %1$d stránek - - %1$s kapitola %1$s kapitoly %1$s kapitol - Odstranit %1$d staženou kapitolu\? Odstranit %1$d stažené kapitoly\? Odstranit %1$d stažených kapitol\? - a %1$d další kapitolu a %1$d další kapitoly a %1$d dalších kapitol - Je dostupná aktualizace rozšíření Jsou dostupné %d aktualizace rozšíření Je dostupných %d aktualizací rozšíření - Jedna kapitola byla z tohoto zdroje odstraněna: \n%2$s @@ -55,100 +43,84 @@ \n \nChcete je smazat\? - %1$d stránka %1$d stránky %1$d stránek - %d aktualizace čeká %d aktualizace čekají %d aktualizací čeká - Přeskočena %d kapitola, buď chybí ve zdroji nebo byla vyfiltrována Přeskočeny %d kapitoly, buď chybí ve zdroji nebo byly vyfiltrovány Přeskočeno %d kapitol, buď chybí ve zdroji nebo byly vyfiltrovány - Rozšíření bylo aktualizováno %d rozšíření byla aktualizována %d rozšíření bylo aktualizováno - Po %1$s minutě Po %1$s minutách Po %1$s minutách - Migrovat %1$d%2$s mangu\? Migrovat %1$d%2$s mangy\? Migrovat %1$d%2$s mang\? - Vyčištění dokončeno. %d odstraněná složka Vyčištění dokončeno. %d odstraněné složky Vyčištění dokončeno. %d odstraněných složek - Další nepřečtená kapitola Další %d nepřečtené kapitoly Dalších %d nepřečtených kapitol - Kopírovat %1$d%2$s mangu\? Kopírovat %1$d%2$s mangy\? Kopírovat %1$d%2$s mang\? - Mezipaměť vymazána. %d soubor byl odstraněn Mezipaměť vymazána. %d soubory byly odstraněny Mezipaměť vymazána. %d souborů bylo odstraněno - %d zdroj %d zdroje %d zdrojů - %d druh sérií %d druhy sérií %d druhů sérií - Pro %d titul Pro %d tituly Pro %d titulů - %d manga migrována %d manga migrovány %d manga migrováno - %d jazyk %d jazyky %d jazyků - %d status %d statusy %d statusů - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/cs/strings.xml b/i18n/src/commonMain/moko-resources/cs/strings.xml index 60fadafcb3..66393f73bf 100644 --- a/i18n/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n/src/commonMain/moko-resources/cs/strings.xml @@ -536,7 +536,6 @@ Podle čísla kapitoly Podle pořadí zdroje Nezaloženo - Je vyžadováno oprávnění k souborům Nastavit jako výchozí Vlastní info o manze Zdroj není nainstalován @@ -618,9 +617,6 @@ Varování: hromadné stahování může vést k tomu, že zdroje zpomalí a/nebo zablokují Tachiyomi. Klepnutím se dozvíte více. Každé 3 dny Snižuje pruhování barev, ale může mít vliv na výkon - TachiyomiJ2K požaduje přístup ke všem souborům v Android 11, aby mohlo stahovat kapitoly, vytvářet zálohy a načíst lokálně uloženou mangu. -\n -\nNa obrazovce povolte \"Povolit aplikaci přístup ke všem souborům.\" Rozšíření čekající na aktualizaci Čtení %1$s Ve skupině @@ -634,7 +630,6 @@ Oříznout okraje (Strankované) Rozdělit dvojité strany Vyplnit vystřihnuté oblasti - TachiyomiJ2K potřebuje přístup k celému úložišti, aby mohlo stahovat kapitoly. Klikněte sem, poté povolte \"Povolit aplikaci přístup k souborům.\" Zobrazovat přečtené kapitoly v Ve skupině a Všechny Sdílet kombinované strany Chytře (podle strany) @@ -905,7 +900,6 @@ 5% Výchozí řetězec uživatelského agenta Některé jazyky mohou pro správné zobrazení vyžadovat opětovné spuštění aplikace - Formát RARv5 není podporován Všechna přečtená manga Ponechejte mangu s přečtenými kapitolami Automatické stahování při čtení @@ -961,4 +955,4 @@ Nastavení aplikace Ladící informace Činnost na pozadí - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/cv/plurals.xml b/i18n/src/commonMain/moko-resources/cv/plurals.xml index 8a2d53ddea..d22780c11a 100644 --- a/i18n/src/commonMain/moko-resources/cv/plurals.xml +++ b/i18n/src/commonMain/moko-resources/cv/plurals.xml @@ -4,44 +4,32 @@ Хушма валли ҫӗнетӳ пур %d хушма валли ҫӗнетӳ пур - %1$s сыпӑк %1$s сыпӑк - %d пухмӑш %d пухмӑш - - - %1$d эл юлчӗ - %1$d эл юлчӗ - - %1$d тиенӗ сыпӑка катертмелле-и\? %1$d тиенӗ сыпӑксене катертмелле-и\? - %1$s,%2$s йӑнӑшпа тӑвӑннӑ%1$s, %2$s йӑнӑшпа тӑвӑннӑ - 1 минут хыҫҫӑн%1$s минут хыҫҫӑн - Ҫӗнӗ сыпӑксем 1 хайлав валли тупӑннӑҪӗнӗ сыпӑксем %d хайлав валли тупӑннӑ - 1 сыпӑк ҫук %d сыпӑк ҫук - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/cv/strings.xml b/i18n/src/commonMain/moko-resources/cv/strings.xml index e9b4bf29a5..02f280b817 100644 --- a/i18n/src/commonMain/moko-resources/cv/strings.xml +++ b/i18n/src/commonMain/moko-resources/cv/strings.xml @@ -398,7 +398,6 @@ Пур ҫӗрте шыра Пур тиеве катертмелле-и\? Ҫак серилӗх валли пурне те пӑрахӑҫла - Файлсене курма ирӗк памалла Сыпӑк шучӗпе Тиесе илни вӑхӑчӗпе Яланхилле @@ -466,4 +465,4 @@ Сӑрӑ Ларткӑч Вулавӑшри серилӗхсем - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/de/plurals.xml b/i18n/src/commonMain/moko-resources/de/plurals.xml index 460dd138be..bfe82030cd 100644 --- a/i18n/src/commonMain/moko-resources/de/plurals.xml +++ b/i18n/src/commonMain/moko-resources/de/plurals.xml @@ -4,37 +4,26 @@ Entferne %1$d heruntergeladenes Kapitel? Entferne %1$d heruntergeladene Kapitel? - %1$s Kapitel %1$s Kapitel - - - %1$d Seite übrig - %1$d Seiten übrig - - %d Kategorie %d Kategorien - Für %d Titel Für %d Titel - und %1$d weiteres Kapitel und %1$d weitere Kapitel - Erweiterungsaktualisierung verfügbar %d Erweiterungsaktualisierungen verfügbar - Ein Kapitel wurde von der Quelle entfernt: \n%2$s @@ -44,84 +33,68 @@ \n \nDiese heruntergeladenen Dateien löschen\? - %1$d%2$s Manga migrieren? %1$d%2$s Manga migrieren? - %1$d%2$s Manga kopieren? %1$d%2$s Manga kopieren? - %d Manga migriert %d Manga migriert - Zwischenspeicher geleert. %d Datei wurde gelöscht Zwischenspeicher geleert. %d Dateien wurde gelöscht - Aufräumen abgeschlossen. %d Ordner entfernt Aufräumen abgeschlossen. %d Ordner entfernt - Nach %1$s Minute Nach %1$s Minuten - Erledigt in %1$s mit %2$s Fehler Erledigt in %1$s mit %2$s Fehlern - %1$d Seite %1$d Seiten - %d Aktualisierung ausstehend %d Aktualisierungen ausstehend - Erweiterung aktualisiert %d Erweiterungen aktualisiert - %d Kapitel wird übersprungen, da die Quelle dieses entweder nicht besitzt, oder weil es rausgefiltert wurde %d Kapitel werden übersprungen, da die Quelle diese entweder nicht besitzt, oder weil sie rausgefiltert wurden - Nächstes ungelesenes Kapitel Nächste %d ungelesene Kapitel - %d Sprache %d Sprachen - %d Quelle %d Quellen - %d Status %d Status - %d Serientyp %d Serientypen - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/de/strings.xml b/i18n/src/commonMain/moko-resources/de/strings.xml index 69d56fed17..45f7f2e0f4 100644 --- a/i18n/src/commonMain/moko-resources/de/strings.xml +++ b/i18n/src/commonMain/moko-resources/de/strings.xml @@ -762,11 +762,6 @@ Globale Aktualisierungen Eine beliebige Serie öffnen Anzahl der Elemente anzeigen - TachiyomiJ2K benötigt zum Herunterladen von Kapiteln Zugriff auf alle Dateien. Tippen Sie hier und aktivieren Sie dann „Zugriff auf die Verwaltung aller Dateien zulassen.“ - TachiyomiJ2K benötigt Zugriff auf alle Dateien in Android 11 um Kapitel herunterzuladen, automatische Backups zu erstellen und lokale Manga zu lesen. -\n -\nAktivieren Sie im nächsten Bildschirm „Zugriff auf die Verwaltung aller Dateien zulassen.“ - Dateiberechtigungen erforderlich Benachrichtigungsinhalt verbergen Quelle wird nicht unterstützt Ausrichtung @@ -940,7 +935,6 @@ 5% Standard-User-Agent-Text Manche Sprachen benötigen möglicherweise einen Neustart der App zur korrekten Anzeige - Das RARv5-Format wird nicht unterstützt Alle gelesenen Manga Manga mit gelesenen Kapiteln behalten Automatisch während dem Lesen herunterladen @@ -997,4 +991,4 @@ App-Einstellungen Debug-Info Hintergrundaktivität - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/el/plurals.xml b/i18n/src/commonMain/moko-resources/el/plurals.xml index 72b3e20cd0..a26e7b82f1 100644 --- a/i18n/src/commonMain/moko-resources/el/plurals.xml +++ b/i18n/src/commonMain/moko-resources/el/plurals.xml @@ -4,67 +4,50 @@ Διαθέσιμη ενημέρωση επέκτασης %d διαθέσιμες ενημερώσεις επεκτάσεων - και %1$d περισσότερο κεφάλαιο και %1$d περισσότερα κεφάλαια - Για %d τίτλο Για %d τίτλους - %d κατηγορία %d κατηγορίες - - - Απομένει %1$d σελίδα - Απομένουν %1$d σελίδες - - Διαγραφή %1$d κατεβασμένου κεφαλαίου; Διαγραφή %1$d κατεβασμένων κεφαλαίων; - Έγινε σε %1$s με %2$s σφάλμα Έγινε σε %1$s με %2$s σφάλματα - Μετά από %1$s λεπτό Μετά από %1$s λεπτά - Ο καθαρισμός ολοκληρώθηκε. Αφαιρέθηκε %d φάκελος Ο καθαρισμός ολοκληρώθηκε. Αφαιρέθηκαν %d φάκελοι - Η προσωρινή μνήμη διαγράφηκε. %d αρχείο διαγράφηκε Η προσωρινή μνήμη διαγράφηκε. %d αρχεία διαγράφηκαν - %d σειρά μετεγκαταστήθηκε %d σειρές μετεγκαταστήθηκαν - Αντιγραφή %1$d%2$s σειρά; Αντιγραφή %1$d%2$s σειρών; - Μετεγκατάσταση %1$d%2$s σειρά; Μετεγκατάσταση %1$d%2$s σειρών; - Ένα κεφάλαιο καταργήθηκε από την πηγή: \n%2$s @@ -74,54 +57,44 @@ \n \nΔιαγραφή των λήψεων τους; - %1$d σελίδα %1$d σελίδες - %1$s κεφάλαιο %1$s κεφάλαια - %d εκκρεμεί ενημέρωση %d εκκρεμούν ενημερώσεις - Η επέκταση ενημερώθηκε %d επεκτάσεις ενημερώθηκαν - Παραλείπεται %d κεφάλαιο, είτε λείπει από την πηγή είτε έχει φιλτραριστεί Παραλείπονται %d κεφάλαια, είτε λείπουν από την πηγή είτε έχουν φιλτραριστεί - Επόμενο αδιάβαστο κεφάλαιο Επόμενα %d αδιάβαστα κεφάλαια - Τύπος σειράς %d τύποι σειρών - Πηγή %d Πηγές - Κατάσταση %d καταστάσεις - Γλώσσα %d γλώσσες - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/el/strings.xml b/i18n/src/commonMain/moko-resources/el/strings.xml index 2f890850bc..5c93a9d755 100644 --- a/i18n/src/commonMain/moko-resources/el/strings.xml +++ b/i18n/src/commonMain/moko-resources/el/strings.xml @@ -741,11 +741,6 @@ Κατά αριθμό κεφαλαίου Κατά σειρά πηγής Χωρίς σελιδοδείκτη - Το TachiyomiJ2K απαιτεί πρόσβαση σε όλα τα αρχεία για να κατεβάσετε κεφάλαια. Πατήστε εδώ και στη συνέχεια, ενεργοποιήστε την επιλογή \"Να επιτρέπεται η πρόσβαση στη διαχείριση όλων των αρχείων.\" - Το TachiyomiJ2K απαιτεί πρόσβαση σε όλα τα αρχεία στο Android 11 για να κατεβάζει κεφάλαια, να δημιουργεί αυτόματα αντίγραφα ασφαλείας και να διαβάζει τις τοπικά αποθηκευμένες σειρές. -\n -\nΣτην επόμενη οθόνη, ενεργοποιήστε την επιλογή \"Να επιτρέπεται η πρόσβαση στη διαχείριση όλων των αρχείων.\" - Απαιτείται άδεια πρόσβασης αρχείων Tako A Calmer You (Δυναμικό) Προειδοποίηση @@ -905,7 +900,6 @@ Βιολετί 5% Προεπιλεγμένη συμβολοσειρά πράκτορα χρήστη - Η μορφή RARv5 δεν υποστηρίζεται Ορισμένες γλώσσες ενδέχεται να απαιτούν επανεκκίνηση της εφαρμογής για να εμφανιστούν σωστά Διατήρηση σειρών με αναγνωσμένα κεφάλαια Όλες οι σειρές που διαβάστηκαν @@ -964,4 +958,4 @@ Εφαρμογή Συνιστάται να επιτρέψετε τις ειδοποιήσεις για να διατηρείται την βιβλιοθήκη σας και την εφαρμογή σας ενημερωμένες. Κοινοποίηση εξωφύλλου - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/eo/plurals.xml b/i18n/src/commonMain/moko-resources/eo/plurals.xml index 8ff7da8010..b20e14c445 100644 --- a/i18n/src/commonMain/moko-resources/eo/plurals.xml +++ b/i18n/src/commonMain/moko-resources/eo/plurals.xml @@ -1,42 +1,31 @@ - - Cetere %1$d paĝo - Cetere %1$d paĝoj - - Forigi %1$d elŝutitan ĉapitron\? Forigi %1$d elŝutitajn ĉapitrojn\? - 1 ĉapitro %1$s ĉapitroj - %d kategorio%d kategorioj - Farita en %1$s kun %2$s eraro Farita en %1$s kun %2$s eraroj - Mankas 1 ĉapitron Mankas %d ĉapitrojn - Post 1 minutoPost %1$s minutoj - Por 1 titoloPor %d titoloj - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/eo/strings.xml b/i18n/src/commonMain/moko-resources/eo/strings.xml index 446eef6ec5..8721108a43 100644 --- a/i18n/src/commonMain/moko-resources/eo/strings.xml +++ b/i18n/src/commonMain/moko-resources/eo/strings.xml @@ -169,7 +169,6 @@ Servoj Ĉi tiu kromaĵo ne estas de la oficiala Tachiyomi kromaĵlisto. Sekura ekrano - Agordi kiel kovrilo Eraro dumo kovrila kunigado Eraro dum kovrila konservado @@ -362,4 +361,4 @@ Devigi malŝlosi Griza Biblioteka kontribuoj - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/es/plurals.xml b/i18n/src/commonMain/moko-resources/es/plurals.xml index 7ca91a260c..b7c186236d 100644 --- a/i18n/src/commonMain/moko-resources/es/plurals.xml +++ b/i18n/src/commonMain/moko-resources/es/plurals.xml @@ -5,37 +5,26 @@ %d actualizaciones de extensiónes disponibles %d actualizaciones de extensiones disponibles - %d categoría %d categorías %d categorías - - - %1$d página restante - %1$d páginas restantes - %1$d páginas restantes - - ¿Eliminar %1$d capítulo descargado\? ¿Eliminar %1$d capítulos descargados\? ¿Eliminar %1$d capítulos descargados\? - y %1$d capítulo más y %1$d capítulos más y %1$d capítulos más - Para %d título Para %d títulos Para %d títulos - Un capítulo ha sido eliminado de la fuente: \n%2$s @@ -49,106 +38,89 @@ \n \n¿Eliminar capítulos descargados\? - Después de %1$s minuto Después de %1$s minutos Después de %1$s minutos - Limpieza realizada. Carpeta %d eliminada Limpieza realizada. Carpetas %d eliminadas Limpieza realizada. Carpetas %d eliminadas - Completada en %1$s con %2$s error Completada en %1$s con %2$s errores Completada en %1$s con %2$s errores - %d serie migrada %d series migradas %d series migradas - ¿Copiar la serie %1$d%2$s\? ¿Copiar las series %1$d%2$s\? ¿Copiar las series %1$d%2$s\? - ¿Migrar la serie %1$d%2$s\? ¿Migrar las series %1$d%2$s\? ¿Migrar las series %1$d%2$s\? - %1$s capítulo %1$s capítulos %1$s capítulos - Caché borrada. Se ha eliminado el archivo %d Caché borrada. Se han eliminado archivos %d Caché borrada. Se han eliminado archivos %d - %1$d página %1$d páginas %1$d páginas - %d actualización pendiente %d actualizaciones pendientes %d actualizaciones pendientes - Extensión actualizada %d extensiones actualizadas %d extensiones actualizadas - Se omite %d capítulo, o bien falta en la fuente o ha sido filtrado Se omiten %d capítulos, o bien faltan en la fuente o han sido filtrados Se omiten %d capítulos, o bien faltan en la fuente o han sido filtrados - El siguiente capítulo sin leer Los siguientes %d capítulos sin leer Los siguientes %d capítulos sin leer - %d fuente %d fuentes %d fuentes - %d Lenguaje %d lenguajes %d Otros lenguajes - %d estado %d estados %d estados - %d tipos de serie %d tipos de series %d tipos de series - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/es/strings.xml b/i18n/src/commonMain/moko-resources/es/strings.xml index 11efa99f35..480554f8e1 100644 --- a/i18n/src/commonMain/moko-resources/es/strings.xml +++ b/i18n/src/commonMain/moko-resources/es/strings.xml @@ -184,7 +184,7 @@ Añadir Conexión a la red no disponible Descarga detenida - ¿Deseas fijar esta imagen como portada\? + ¿Usar esta imagen como portada? Cerrar Mostrar Cuadrícula compacta @@ -239,8 +239,8 @@ ¡Ya existe una categoría con este nombre! Imagen guardada Opciones - Establecer como cubierta - Cubierta actualizada + Establecer como portada + Portada actualizada Error al actualizar la portada Insignias de descarga Local @@ -435,7 +435,7 @@ Ocultar tolva de categoría Mostrar como Toca el icono de la Biblioteca para mostrar los filtros - Cubiertas uniformes + Portadas uniformes Ocultar el botón de inicio de lectura Cuadrícula agradable Opciones de visualización @@ -473,7 +473,7 @@ Portada guardada Fuente no instalada Recordar selección - Recolocar portada + Restablecer portada Usar predefinida Etiqueta Ordenar y filtrar @@ -616,8 +616,7 @@ Mover páginas dobles Mover únicamente una página Añadido a %1$s - Elimina las portadas en la caché y no utilizadas de las entradas de la biblioteca que se han actualizado. -\nActualmente estás usando: %1$s + Elimina las portadas almacenadas en caché antiguas y sin uso de las entradas de tu biblioteca que se han actualizado. \nActualmente estás usando: %1$s Sin iniciar sesion en %1$s Rosa chicle Florecer de primavera @@ -659,7 +658,7 @@ Igualar fuentes activadas Igualar fuentes marcadas Buscar solo en fuentes fijadas - Eliminar portadas en la caché fuera de la biblioteca + Borrar portadas almacenadas en caché que no están en la biblioteca Iniciando limpieza Limpiar las entradas que no pertenezcan a la biblioteca Eliminar carpetas de capítulos inexistentes, parcialmente descargados o leídos @@ -705,7 +704,7 @@ Ciertos botones se pueden encontrar en otros sitios estando esto desactivado Botones abajo del lector Tamaño de la cuadrícula - Cuadrículas con cubiertas uniformes + Cuadrículas con portadas uniformes También se puede encontrar al expandir los filtros de la biblioteca Expandir / Colapsar todas las categorías Al agrupar la biblioteca por fuentes, estados, etc. @@ -760,11 +759,6 @@ Actualizaciones globales Abrir una serie aleatoria Mostrar el número de elementos - TachiyomiJ2K requiere acceso a todos los archivos para descargar capítulos. Toque aquí y, acto seguido, active \"Acceso a todos los archivos.\" - TachiyomiJ2K requiere acceso a todos los archivos en Android 11 para descargar los capítulos, crear las copias de seguridad automáticas y leer las series locales. -\n -\nEn la siguiente pantalla, activa \"Permitir el acceso para gestionar todos los archivos\" - Se necesita el permiso de archivos Advertencia Orientación Orientación predeterminada @@ -810,7 +804,7 @@ Extensiones actualizadas Instalado %1$s Actualizaciones de la extensión pendientes - Afecta a la rejilla de la biblioteca + Afecta a las portadas de la cuadricula de la biblioteca Restricciones: %1$s Ningún resultado con tus filtros Mostrar categorías vacías al filtrar @@ -822,7 +816,7 @@ Ayudar a traducir Filtrar grupos de scanlations El seguimiento no se puede retirar de %1$s mientras esta fuera de línea - Botones del tema basados en la cubierta + Botones del tema basados en la portada Cerrar sesión ¿Cerrar sesión de %1$s\? Eliminar de %1$s @@ -935,7 +929,6 @@ Violeta 5% Nombre del navegador a usar («user agent») - La app no soporta el formato RARv5 Algunos idiomas pueden requerir un reinicio de la aplicación para mostrarse correctamente Guardar las entradas con los capítulos leídos Todas las series leídas @@ -994,4 +987,89 @@ Información sobre la depuración Actividad en segundo plano Compartir la portada + Seleccione una carpeta donde %1$s almacenará descargas de capítulos, copias de seguridad y más.\n\nSe recomienda una carpeta dedicada.\n\nCarpeta seleccionada: %2$s + Para instalar la aplicación en actualizaciones. + Seleccionemos algunos valores predeterminados. Puedes cambiarlos más adelante en la configuración. + Actualizaciones + Evite interrupciones en las actualizaciones de bibliotecas, descargas y restauraciones de copias de seguridad de larga duración. + Toca dos veces para hacer zoom + Imprimir registros detallados en el registro del sistema (puede reducir el rendimiento de la aplicación) + Registro detallado + Repositorios de extensiones + Aún no has agregado ningún repositorio. + El repositorio %1$s tiene la misma huella de clave de firma que %2$s.\nSi esto es lo esperado, %2$s será reemplazado; de lo contrario, contacte con el responsable del repositorio. + Ubicación de almacenamiento + Recortar bordes (Long strip) + %s se encontró con un error inesperado. Le sugerimos que tome una captura de pantalla de este mensaje, descargue los registros de errores y luego lo comparta en el repositorio de GitHub. + Justo ahora + Usar la biblioteca de composición experimental + Aleatorio + Habilitar la acción de deslizar el capítulo + Extensiones maliciosas podrían leer las credenciales de inicio de sesión almacenadas o ejecutar código arbitrario.\n\nAl confiar en esta extensión, acepta estos riesgos. + Revocar todas las extensiones de confianza + ¿Revocar todas las extensiones de confianza? + No se pudo obtener acceso persistente a la carpeta. La aplicación podría tener un comportamiento inesperado. + Toque aquí para obtener ayuda con Cloudflare + Se requiere WebView para que la aplicación funcione + Atrás + Adelante + Actualizar + Error interno: %s + SFW + NSFW + Tipo de contenido + Se Produjo Un Error Inesperado + Reiniciar la aplicación + Refrescar + Selecciona una carpeta + Mostrar cola de descarga + Abrir último capítulo leído + Comportamiento al pulsar prolongadamente en Recientes + Abrir búsqueda global + Comportamiento al pulsar prolongadamente en Explorar + Abrir el menú de extensiones / migración + Mostrar contenido en el área recortada + Perfil de visualización personalizado + Datos y almacenamiento + Almacenamiento usado + Disponible: %1$s / Total: %2$s + Puede solucionar el problema con los conflictos entre los capítulos descargados cuando tienen el mismo nombre + Auto-anexar ID + ¿Actualizas desde una versión anterior y no sabes qué elegir? Consulta la sección de actualización de Tachiyomi en la guía de almacenamiento de Mihon para más información. + Guía de almacenamiento + Obligatorio + Opcional pero recomendado + Permiso para instalar aplicaciones + Permiso de notificación + Reciba notificaciones sobre actualizaciones de la biblioteca y más. + Uso de batería en segundo plano + Permitir + No hay ninguna ubicación de almacenamiento establecida + Ubicación no válida: %s + Ubicación no válida + Página %1$d de %2$d + No seleccionado + Seleccionado + Abrir una serie aleatoria (Global) + Default + El instalador Legacy aún no está implementado; actualmente se recurre al PackageInstaller (default) + Guardar páginas en carpetas separadas + Crea carpetas según el título del manga + Si el lector carga una imagen en blanco, reduzca gradualmente el umbral(Threshold).\nSeleccionado: %s + Incluir configuraciones sensibles (p. ej. tokens de inicio de sesión de trackers) + Última copia de seguridad automática: %s + Agregar nuevo repositorio + Repositorio de código abierto + Modo de depuración + Escanear almacenamientos externos en busca de entradas + Agregar repositorio + URL de repositorio no válida + ¡El repositorio ya existe! + ¿Eliminar repositorio? + ¿Está seguro de que desea eliminar el repositorio \"%s\"? + Reemplazar + La huella digital de la clave de firma ya existe + No se puede abrir la URL + Mover la serie al final + Habilitar la acción de deslizar la fuente diff --git a/i18n/src/commonMain/moko-resources/eu/plurals.xml b/i18n/src/commonMain/moko-resources/eu/plurals.xml index afc55abfd9..9dded181a7 100644 --- a/i18n/src/commonMain/moko-resources/eu/plurals.xml +++ b/i18n/src/commonMain/moko-resources/eu/plurals.xml @@ -4,27 +4,22 @@ Kendu kapitulu %1$d\? Kendu %1$d kapitulu\? - Kapitulu bat saltatzen, iturria falta da edo iragazia izan da %d kapitulu saltatzen, iturria falta da edo iragaziak izan dira - %d kategoria %d kategoriak - %1$d orrialdea %1$d orrialdeak - %d manga migratu da %d manga migratu dira - Kapitulu bat iturritik kendua izan da: \n%2$s @@ -34,69 +29,52 @@ \n \nHaien deskargak ezabatu\? - Migratu manga %1$d%2$s\? Migratu %1$d%2$s manga\? - Kopiatu manga %1$d%2$s\? Kopiatu %1$d%2$s manga\? - Garbiketa egina. Karpeta %d kendu da Garbiketa egina. %d karpeta kendu dira - Cachea garbitu da. Fitxategi %d ezabatu da Cachea garbitu da. %d fitxategi ezabatu dira - Minutu %1$s-en ondoren %1$s minuturen ondoren - Luzapena eguneratu da %d Luzapen eguneratu dira - %d tituluarentzako %d tituluentzako - eta kapitulu %1$d gehiago eta %1$d kapitulu gehiago - eguneraketa %d egiteke %d eguneraketa egiteke - Luzapenaren eguneraketa eskuragarri %d Luzapenen eguneraketak eskuragarri - %1$s-n egin da errore %2$s-ekin %1$s-n egin da %2$s errorerekin - %1$s kapitulu %1$s kapituluak - - - Orri %1$d geratzen da - %1$d orri geratzen dira - - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/eu/strings.xml b/i18n/src/commonMain/moko-resources/eu/strings.xml index 1ab5ee28b1..910d3d2233 100644 --- a/i18n/src/commonMain/moko-resources/eu/strings.xml +++ b/i18n/src/commonMain/moko-resources/eu/strings.xml @@ -1,8 +1,5 @@ - TachiyomiJ2K-k Android 11-kako fitxategi guztietara sarrera behar du kapituluak jaitsi, segurtasun kopia automatikoak sortu, eta manga era lokalean irakurtzeko. -\n -\nHurrengo pantailan, aukeratu \"Baimendu fitxategi guztietarako sarrera.\" %1$s eguneratze-zerrendara gehitzen Kategoria ezabatua Kategoria berria sortu @@ -67,8 +64,6 @@ Etiketa Desblokeatu Desblokeatu liburutegira sartzeko - Fitxategi baimenak behar dira - TachiyomiJ2K-k fitxategi guztietara sartzeko baimena behar du kapituluak jaisteko. Egin klik hemen, eta aukeratu \"Baimendu fitxategiak kudeatzeko sarrera.\" Manga Manhwa Manhua @@ -861,4 +856,4 @@ Baztertutako kategoriak Instalatzailea Atzeko planoko ekintzak - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/fa/strings.xml b/i18n/src/commonMain/moko-resources/fa/strings.xml index a0bc89601d..58e92453a8 100644 --- a/i18n/src/commonMain/moko-resources/fa/strings.xml +++ b/i18n/src/commonMain/moko-resources/fa/strings.xml @@ -303,7 +303,6 @@ آغاز نشده کامیک مانهوای - محلی پشتیبان گیری خودکار فایل پشتیبان نامعتبر است @@ -444,10 +443,6 @@ پاک کردن سابقه سابقه پاک شد آیا مطمئن هستید؟ تمام سابقه از دست خواهد رفت. - اجازه دسترسی به فایل - تاچیومی J2K نیازمند دسترسی به همه فایل ها در اندوید 11 برای دانلود قسمت ها، ایجاد پشتیبان گیری خودکار، و خواندن مانگاهای محلی( مانگا های موجود در دستگاه شما) است. -\n -\nدر صفحه بعدی، اجازه \"مجاز بودن دسترسی به مدیریت همه پرونده‌ها\" را بدهید. مدیریت اعلان‌ها رد شده راهنمای شروع @@ -501,7 +496,6 @@ نصف کردن خودکار عکس های بلند مناطق قابل لمس نام دسته نمی تواند خالی باشد - TachiyomiJ2K برای دانلود فصل ها نیاز به دسترسی به همه فایل ها دارد. روی اینجا ضربه بزنید، سپس «Allow access to management all files» را فعال کنید. در حال بروز رسانی %1$s به روز رسانی تکمیل شد نکات جستجو به صورت دوره ای نمایش داده می شود. برای جستجوی، پیشنهاد را به مدت طولانی فشار دهید. @@ -541,4 +535,4 @@ نصب کننده کتابخانه ورودی‌ها فعالیت در پس زمینه - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/fi/plurals.xml b/i18n/src/commonMain/moko-resources/fi/plurals.xml index 7cd1316e99..da473dce52 100644 --- a/i18n/src/commonMain/moko-resources/fi/plurals.xml +++ b/i18n/src/commonMain/moko-resources/fi/plurals.xml @@ -1,56 +1,41 @@ - Laajennospäivitys saatavilla %d laajennospäivitystä saatavilla - %d kategoria %d kategoriaa - - - %1$d sivu jäljellä - %1$d sivua jäljellä - - Poistetaanko %1$d ladattu luku\? Poistetaanko %1$d ladattua lukua\? - Nimikkeelle %d Nimikkeille %d - - Valmistui %1$s virheitä löytyi %2$s - Valmistui %1$s virheitä löytyi %2$s + "Valmistui %1$s ja %2$s virhe" + "Valmistui %1$s ja %2$s virhettä" - Välimuisti tyhjennetty. %d tiedosto on poistettu Välimuisti tyhjennetty. %d tiedostoa on poistettu - %d manga siirretty %d mangaa siirretty - Kopioi %1$d%2$s manga\? Kopioi %1$d%2$s mangasta\? - Siirrä %1$d%2$s manga\? Siirrä %1$d%2$s mangaa\? - Luku on poistettu lähteestä: \n%2$s @@ -60,39 +45,56 @@ \n \nPoistetaanko niiden lataukset\? - ja %1$d lisää luku ja %1$d lisää lukua - %1$s luku %1$s lukua - Siivous tehty. Poistettu %d kansio Siivous tehty. Poistettu %d kansiota - %1$s minuutin jälkeen %1$s minuutin jälkeen - %1$d sivu %1$d sivua - Seuraava lukematon luku Seuraavat %d lukematonta lukua - 1 puuttuva luku %d puuttuvaa lukua + + %d odottava päivitys + %d odottavaa päivitystä + + + %d laajennus päivitetty + %d laajennusta päivitetty + + + %d sarjatyyppi + %d sarjatyyppiä + + + %d lähde + %d lähdettä + + + %d tila + %d tilaa + + + %d kieli + %d kieltä + diff --git a/i18n/src/commonMain/moko-resources/fi/strings.xml b/i18n/src/commonMain/moko-resources/fi/strings.xml index 27a23de06d..e47d743b5c 100644 --- a/i18n/src/commonMain/moko-resources/fi/strings.xml +++ b/i18n/src/commonMain/moko-resources/fi/strings.xml @@ -529,7 +529,7 @@ Nollaa kaikki %1$s luvut Tämä poistaa \"%1$s\" -lukupäivän. Oletko varma\? Äskettäin lisätty - Viimeisimmät + Viimeaikaiset Päivitä kirjaston kannet myös kirjastoa päivitettäessä Päivitä kannet automaattisesti Sisällytä globaaliin päivitykseen @@ -706,7 +706,6 @@ Merkitse useat luvut luetuiksi Peruuta kaikki tälle sarjalle Sovelluksen pikavalinnat - Tiedoston käyttöoikeudet vaaditaan Piilota ilmoitusten sisältö Lukuun ottamatta: %s Lähdettä ei tueta @@ -722,7 +721,7 @@ Virheet Ei kirjanmerkeissä Lähteen järjestyksen mukaan - Laajat päivitykset voivat aiheuttaa suurempaa akun kulutusta, sekä lähteiden päivitysten hidastumista + Laajat päivitykset voivat johtaa akun käytön lisääntymiseen ja lähteiden hidastumiseen. Napauta saadaksesi lisätietoja. Varoitus: massalataukset voivat johtaa siihen, että lähteet muuttuvat hitaammiksi käyttää ja/tai ne estävät Tachiyomin käytön. Napauta saadaksesi lisätietoja. Vaikuttaa kirjaston kansiin ruudukossa Päivitystä ei voitu asentaa @@ -782,7 +781,6 @@ Automaattinen lataus luetessa %1$d sarjaa joita ei olla lisätty kirjastoon tietokanassa Parannetut palvelut - RARv5-muoto ei ole tuettu Korkea Alhaisin Ladattua kuvaa ei voitu jakaa @@ -817,4 +815,91 @@ Poissuljetut kategoriat Asentaja Taustatoiminta + Varastoinnin opas + Näytä lisää lukuja + Ota luvun pyyhkäisytoiminto käyttöön + Päivitä sarja vain, jos siinä ei ole lukematonta lukua (lue kokonaan) + Poistetaanko edellinen jäljitin? + Satunnainen + Odottavat laajennuspäivitykset + MIUI-optimointi on poistettava käytöstä, jotta laajennukset voidaan asentaa. + Kumotetaanko kaikki luotetut laajennukset? + Jos lukija lataa tyhjän kuvan asteittain, pienennä kynnystä.\nValittu: %s + Näytä %1$s + Tämän merkinnän yksityiskohtiensivu on tarkasteltava ennen siirtoa + Lähteen asetukset + Kaikki luetut sarjat + Suodata scanlator-ryhmät + Palvelut, jotka tarjoavat lisäominaisuuksia tietyille lähteille. Merkintöjä jäljitetään automaattisesti, kun ne lisätään kirjastoosi. + Pitkä napautus Selauskäyttäytyminen + Käytä kokeellista luomiskirjastoa + Käytä porrastettua ruudukkoa + Haitalliset laajennukset voivat lukea mitä tahansa tallennettuja kirjautumistietoja tai suorittaa mielivaltaisen koodin.\n\nLuottamalla tähän laajennukseen hyväksyt nämä riskit. + Asennettu %1$s + Luo kansioita mangan otsikon mukaan + Herkkyys valikon piilottamiseen vierityksen yhteydessä + Sovelluksen asetukset + Päivitä jäljittäminen, kun se merkitään luetuksi + Päivitä jäljittäminen lukemisen jälkeen + Valitse merkintä + Lisää vaihtoehtoja + Navigoi ylös + Vaadittu + Valinnainen mutta suositeltavaa + Asenna sovellusten lupa + Sovelluksen asentaminen päivityksten yhteydessä. + Ilmoituslupa + Tallenna sivut erillisiin kansioihin + Jaa kansi + Tervetuloa! + Valitsemme joitain oletusasetuksia. Voit aina muuttaa näitä asioita myöhemmin asetuksista. + Aloita + Valitse kansio, johon %1$s tallentaa lukulataukset, varmuuskopiot ja paljon muuta.\n\nErillinen kansio on suositeltavaa.\n\nValittu kansio: %2$s + Valitse kansio + Päivitämässä vanhemmasta versiosta, eikä varma mitä valita? Katso lisätietoja Mihon-tallennusoppaan Tachiyomi-päivitysosiosta. + Saat ilmoituksen kirjaston päivityksistä ja muusta. + Tausta akun käyttö + Vältä keskeytyksiä pitkäaikaisissa kirjastopäivityksissä, latauksissa ja varmuuskopioiden palautuksissa. + Tallennussijaintia ei ole määritetty + Myönnä + Virheellinen sijainti: %s + Virheellinen sijainti + Kirjaston merkinnät + Sivu %1$d/%2$d + Uusi beta-versio saatavilla! + Ei valittu + Valittu + Avaa satunnainen sarja (laajamittainen) + Kielinimiöt + Ilmoitusten salliminen on suositeltavaa pitääksesi kirjastosi ja sovelluksesi ajan tasalla. + Lajittele noudetun ajan mukaan + Sarjojen mukaan + Viikon mukaan + Päivän mukaan + Näytä latausjono + Avaa viimeksi luettu luku + Kosketa pitkään Viimeaikaisten käyttäytyminen + Avaa laajennukset / siirtovalikko + Avaa laajamittaiinen haku + Oletus + Mahdollistaa laajennusten asentamisen ilman käyttäjän kehotteita ja mahdollistaa automaattiset päivitykset Android 12 -käyttöjärjestelmää käyttäville laitteille + Laajennusta ei voitu asentaa + Jotkut laajennukset saattavat silti pyytää asentamaan ne ensin. + Laajennuksia päivitetään + Laajennukset päivitetty + Kumoa kaikki luotetut laajennukset + Pitkä nauha + Ohita päällekkäiset luvut + Android 9.0:aa vanhemmissa laitteissa sinun on määritettävä katkaisuasetukset manuaalisesti järjestelmäasetuksista + Mukautettu näyttöprofiili + Mukautettu laitteiston bittikartan kynnys + Oletus (%d) + Zoomaa kaksoisnapauttamalla + Lisää tunniste + Odotuslista + Jäljitintä ei voi poistaa kohteesta %1$s ei-verkkotilassa + Yleisesti pätevät päivitykset + Päivitykset + Kirjasto päivitetty viimeksi: %s + Kutista ryhmitetyt luvut diff --git a/i18n/src/commonMain/moko-resources/fil/plurals.xml b/i18n/src/commonMain/moko-resources/fil/plurals.xml index 4911fc8ee5..eeaf928852 100644 --- a/i18n/src/commonMain/moko-resources/fil/plurals.xml +++ b/i18n/src/commonMain/moko-resources/fil/plurals.xml @@ -1,81 +1,61 @@ - %1$s kabanata %1$s (na) kabanata - Available na ang pag-update ng extension Available na ang %d na mga update sa extension - %d kategorya %d (na) kategorya - - - %1$d pahina na lang - %1$d (na) pahina na lang - - Tanggalin ang %1$d na-download na kabanata\? Tanggalin ang %1$d (na) na-download na kabanata\? - Na-restore sa loob ng %1$s na may %2$s error Na-restore sa loob ng %1$s na may %2$s (na) error - Nalinis na ang cache. Binura ang %d file Nalinis na ang cache. Binura ang %d (na) file - %d nakabinbing update %d (na) nakabinbing update - Para sa %d serye Para sa %d (na) serye - at %1$d pang kabanata at %1$d pang mga kabanata - Na-update na ang extension Na-update na ang %d na mga extension - %1$d pahina %1$d (na) pahina - Nilaktawan ang %d na kabanata, maaaring ito ay wala sa source o na-filter ang mga ito Nilaktawan ang %d na mga kabanata, maaaring wala sa source o na-filter ang mga ito - Pagkatapos ng %1$s minuto Pagkatapos ng %1$s (na) minuto - Ilipat ang %1$d manga %2$s\? Ilipat ang %1$d (na) manga %2$s\? - Tinanggal sa source ang kabanata: \n%2$s @@ -85,44 +65,36 @@ \n \nBurahin ang mga na-download\? - Nailipat na ang %d manga Nailipat na ang %d (na) manga - Kopyahin ang %1$d manga %2$s\? Kopyahin ang %1$d (na) manga %2$s\? - Tapos na ang paglilinis. Inalis ang %d folder Tapos na ang paglilinis. Inalis ang %d (na) folder - Susunod na hindi pa nababasa na kabanata Susunod na %d di pa nababasa na kabanata - %d uri ng serye %d mga uri ng serye - %d na pinagkukunan %d na mga pinagmumulan - %d na katayuan %d mga katayuan - %d wika %d na mga wika - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/fil/strings.xml b/i18n/src/commonMain/moko-resources/fil/strings.xml index 7822b1ecd2..c3e6c92695 100644 --- a/i18n/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n/src/commonMain/moko-resources/fil/strings.xml @@ -137,7 +137,7 @@ I-update kung nakamit ang (mga) kondisyon Kategoryang kasama sa panlahatang update Bago - Nakaraan + Kasaysayan Walang nang resulta Walang nakitang resulta Lokal na Source @@ -405,7 +405,7 @@ In-update noong %1$s Tatanggalin nito ang petsa ng pagbasa sa \"%1$s\". Sigurado ka ba\? Mga bago - Bago & Nakaraan + Bago at kasaysayan Isama sa panlahatang update Petsa kinuha Mapusyaw @@ -421,13 +421,13 @@ Mga button sa baba ng pagbasa Ipakita bilang Pagpapakita - Shortcuts ng app + Mga shortcut ng app Itago kapag nag-scroll Itago agad ang nabigasyon sa baba Simula Pindutin ang Balik para pumunta sa simula - Ipagpaubaya - Ipagpaubaya + Gamitin ang default + Default ng sistema Itakda bilang default para sa lahat Parametro sa paghahanap (hal. language:filipino) I-filter ang mga wika @@ -497,7 +497,7 @@ Huling ginamit (Aklatan o Kamakailan) Purong itim Bumalik sa simula - Dami ng kusang pag-backup + Dami ng awtomatikong backup Bigong ma-restore ang backup Maaaring hindi gumana nang maayos ang pag-backup/pag-restore kung nakasara ang MIUI optimization. Bina-backup na @@ -545,23 +545,23 @@ Nito lamangg Ipakita muna ang pamagat Ipakita ang mga nabasang kabanata sa Nakagrupo at Lahat - Ipakita ang I-reset ang nakaraan + Ipakita ang I-reset ang kasaysayan Na-download lang Di pa nabasa Di pa nabasa o na-download Ipakita ang I-download - Mari-reset din ng pagpindot nang matagal ang nakaraan sa kabanata + Pindutin nang matagal ay maaari ding i-reset ang kasaysayan ng kabanata I-reset ang kabanata\? Nakagrupo Walang kamakailang nabasa Walang kamakailang kabanata Hanapin sa kamakailan… - Tingnan ang nakaraan + Tingnan ang kasaysayan Dinagdag %1$s Huling binasa %1$s Nabasa %1$s I-reset ang lahat ng mga kabanata para sa %1$s - I-reset ang nakaraan + I-reset ang kasaysayan Kadadagdag Kamakailan Paminsan-minsang magpapakita ng mga mungkahi sa paghahanap. I-long tap ang mungkahi para hanapin ito. @@ -569,12 +569,7 @@ Makikita ang ilang mga button sa ibang lugar kung nakapatay dito Panlahatang pag-update Rine-refresh ang mga cover sa aklatan at habang nag-a-update - Kusang i-refresh ang mga cover - Kinakailangan ng Yokai ng access sa lahat ng mga file para maka-download ng mga kabanata. I-tap ito, tapos pakibuksan ang \"Payagan ang pamamahala ng lahat ng file.\" - Ang Yokai ay nangangailangan ng access sa lahat ng mga file sa Android 11 upang mag-download ng mga kabanata, gumawa ng mga awtomatikong pag-backup, at magbasa ng lokal na manga. -\n -\nSa susunod na screen, paganahin ang \"Payagan ang pag-access upang pamahalaan ang lahat ng mga file.\" - Kailangan ng permiso sa file + Awtomatikong i-refresh ang mga cover Kapag paalpabetong mag-aayos, ayusin nang binabalewala ang mga article (a, an, the) sa simula ng mga pamagat Ayusin nang binabalewala ang mga article Lumipat nang dalawahang pahina @@ -625,12 +620,12 @@ Baba Mag-download na Babala - Hinihinto ang pagtala sa mga binabasa + Nahihinto ang pagbabasa ng kasaysayan Yotsuba Kasama: %s Presas Mga Gawain - Ipagpaubaya + I-set bilang default Di tumatakbo ang Shizuku Paki-install at buksan ang Shizuku para magamit ito bilang taga-install ng extension. Walang tumugmang nakita @@ -701,7 +696,7 @@ Tanggalin ang pag-track sa app Lumipat na Malinaw na Ikaw (Dynamic) - Buksan agad kung may bagong kabanata + Ang mga shortcut ng serye ay nagbubukas ng mga bagong kabanata Gagana lang sa Patayo ang gagawin sa cutout area para sa mga piling paraan ng pagsasalaki Ipakita ang mga kabanatang na-download Idagdag ang %1$s sa Aklatan\? @@ -729,8 +724,8 @@ Di matatanggal ang tracker mula sa %1$s habang offline Buksan ang %s Maliwanag na tema - Ipagpaubaya - Kung ipagpapaubaya, gagamitin ang nabigasyon sa gilid para sa ilang mga phone at maliliit na tablet habang nakahiga, at palaging magpapakita ito sa mga malalaking tablet + Default na pagkilos + Bilang default, ginagamit ang side navigation para sa ilang mga telepono at maliliit na tablet kapag nasa landscape, at laging ipinapakita sa mas malalaking tablet May bagong beta na bersyon! Tanggalin ang nakaraang tracker\? Yin @@ -741,9 +736,9 @@ Yang Pahina ng detalye Pinalawak na toolbar - Kusang i-update ang app + Awtomatikong i-update ang app Sa kahit anong network - Wag magkusang mag-update + Huwag awtomatikong mag-update Gamitin ang di-pantay na grid I-update lang ang manga kung walang kabanatang di pa nabasa (lahat nabasa) Pareho @@ -757,7 +752,7 @@ Gamitin ang source na may pinakamaraming kabanata (mas mabagal) Isara ang %s Madilim na tema - Kusang mag-update + Awtomatikong mag-update Ipakita ang mga huling ginamit na source Nalinis na ang WebView data Linisin ang WebView data @@ -774,7 +769,7 @@ Hatiin ang dobleng pahina Kung nakapatay, lalaktawan ang pahina ng paglipat kung naka-load na ang susunod na kabanata Simulan ang nakalipas na cutout - Awto (base sa orientation) + Awtomatiko (base sa orientation) Lahat ng kabanata Lahat ng di pa nabasa Parehong panatilihin sa %1$s at palitan lang nang lokal @@ -793,23 +788,23 @@ Takipsilim Lime Time Maaaring humantong sa mga isyu ang pagbalik sa mas lumang bersyon at baka kailanganin nito ang paglilinis sa data ng app. - Burahin ang nakaraan ng mga entry na hindi naka-save sa aklatan mo + Burahin ang kasaysayan ng mga entry na hindi naka-save sa iyong aklatan I-refresh ang metadata ng pagta-track Maaaring di stable ang mga beta at baka kailanganin nito ang paglilinis sa data ng app. Linisin lahat ng mga na-download na kabanata Subukan ang mga bagong tampok bago sila opisyal na mailabas. Maaaring di stable ang mga beta at ginawa upang magbigay ng feedback sa developer. I-refresh ang metadata ng Aklatan - Linisin ang nakaraan - Nalinis na ang nakaraan - Sigurado ka ba talaga\? Mawawala ang buong nakaraan. + Linisin ang kasaysayan + Nalinis na ang kasaysayan + Sigurado ka ba talaga? Mawawala ang buong kasaysayan. I-save bilang CBZ archive Error: walang URI - Rinerekomenda po namin ang kusang pag-backup. Kailangan mo ring magtabi ng mga kopya sa ibang lugar. + Lubos na inirerekomenda ang awtomatikong backup. Dapat ka ring magtago ng mga kopya sa iba pang mga lugar. Pamahalaan ang mga abiso Di pa lowbat Mga Madalas Itanong at Gabay Linisin ang mga filter - Panimulang orientation + Default na orientation Orientation Nai-login na Bawal bakante ang username o password @@ -864,8 +859,8 @@ Walang lilinising folder Patayin ito kung nakakaranas ka ng mga isyu sa pag-update o pag-restore sa Aklatan mo Linisin ang mga naka-cache na cover - Kusang i-update ang mga mga extension - Maaaring hindi kusang mag-update ang ilang mga mga extension kung in-install ito sa labas ng app na ito + Awtomatikong i-update ang mga extension + Maaaring hindi awtomatikong mag-update ang ilang mga extension kung in-install ito sa labas ng app na ito Anong bago sa release na ito Nakatutulong na link sa pagsalin Palaging panatilihin @@ -904,11 +899,10 @@ Lila 5% Default na string ng user agent - Di suportado ang format na RARv5 Maaaring mangailangan ang ilang wika ng muling pag-restart ng app upang maipakita ito Patuloy na magbasa ng mga kabanata Mga nabasang manga - Kusang mag-download habang nagbabasa + Awtomatikong mag-download habang nagbabasa Bilang ng na-download Istatistika Stat @@ -932,7 +926,7 @@ Tracker Haba Sinimulang taon - Oras na ginugol sa pambasa, base sa nakaraang binasa + Oras na ginugol sa reader, base sa kasaysayan ng kabanata Nilaktawan dahil hindi kailangan ang pag-update ng serye Inbalidong string ng user agent Burahin din ang may pananda @@ -960,4 +954,107 @@ Mga setting ng app Impormasyon sa pag-debug Gawaing background + Iba pang opsiyon + Mag-navigate pataas + Custom na threshold sa hardware bitmap + Umiiral na ang repo na ito! + Bago lamang + I-restart ang aplikasyon + Maligayang Pagdating! + Pumili tayo ng ilang default. Maaari mong palaging baguhin ang mga bagay-bagay sa ibang pagkakataon sa mga setting. + Magsimula + Pumili ng folder kung saan mag-imbak ang %1$s ng mga na-download ng kabanata, mga backup, at higit pa. \n \nInirerekomenda ang isang nakalaang folder. \n \nNapiling folder: %2$s + Pumili ng folder + Nag-a-update mula sa isang mas lumang bersyon at hindi sigurado kung ano ang pipiliin? Sumangguni sa Tachiyomi upgrade na seksyon sa Mihon storage guide para sa higit pang impormasyon. + Gabay sa storage + Kinakailangan + Opsyonal ngunit inirerekomenda + Pahintulot sa pag-install ng mga app + Upang ma-install ang app sa mga update. + Pahintulot sa mga abiso + Maabisuhan para sa mga update sa aklatan at higit pa. + Paggamit ng baterya sa background + Payagan + Walang nakatakdang lokasyon ng storage + Imbalidong lokasyon: %s + Imbalidong lokasyon + Pahina %1$d ng %2$d + Magbukas ng random na serye (Pangkahalatan) + Paganahin ang pagkilos na pag-swipe ng kabanata + Mga update + Ipakita ang queue sa pag-download + Buksan ang huling nabasang kabanata + Gawi ng pag-long tap sa Kamakailan + Buksan ang pangkalahatang paghanap + Gawi ng pag-log tap sa Maghanap + Legasiya + Nagbibigay-daan sa mga extension na ma-install nang walang mga prompt ng user at nagbibigay-daan sa mga awtomatikong pag-update para sa mga device sa ilalim ng Android 12 + Default + Hindi pa ipinapatupad ang legacy installer, kasalukuyang bumabalik sa PackageInstaller (Default) + Maaaring basahin ng mga mapanganib na extension ang anumang nakatagong kredensyal sa pag-log in o magsagawa ng arbitrary code. \n \nSa pamamagitan ng pagtitiwala sa extension na ito, tinatanggap mo ang mga panganib na ito. + Bawiin ang mga pinagkakatiwalaang hindi kilalang extension + Bawiin lahat ng mga pinagkatiwalaang mga extension? + I-crop ang mga border (Pahabang strip) + Buksan ang mga setting ng legasiyang cutout + Sa mga device na mas luma sa Android 9.0, kailangan mong itakda nang manu-mano ang mga setting ng cutout sa pamamagitan ng mga setting ng sistema + Ipakita ang nilalaman sa lugar ng cutout + Lumikha ng folder base sa titulo ng manga + Default (%d) + I-double tap para mag-zoom + I-share ang cover + Datos at storage + Lokasyon ng storage + Paggamit ng storage + Na magagamit: %1$s / Kabuuan: %2$s + Kasali ang mga sensitibong setting (hal. mga tracker login token) + Huling awtomatikong na-back up: %s + I-print ang detalyadong mga log sa system log (nakakabawas sa performance ng app) + Mode sa pag-debug + Verbose na pagla-log + Maghanap sa mga external na storage para sa mga entry + Mga Repo ng Extension + Magdagdag ng repo + Idagdag ang repo + Di-wastong URL ng repo + Hindi ka pa nagdaragdag ng anumang mga repo. + Tanggalin ang repo? + Gusto mo bang tanggalin ang repo na \"%s\"? + Palitan + Umiiral na ang Signing Key Fingerprint + Ang repository na %1$s ay may magkaparehong Signing Key Fingerprint sa %2$s. \nKung ito ay inaasahan, %2$s ang papalitan, kung hindi naman ay makipag-ugnayan sa tagapamahala ng iyong repo. + Open source na repo + Hindi mabuksan ang url + Ilagay sa ibaba ang serye + Awtomatikong idagdag ang ID + I-tap dito para sa tulong sa Cloudflare + Kinakailangan ng app ang WebView upang gumana ito + Nabigong makakuha ng patuloy na pag-access ng folder. Ang app ay magkaroon ng di-inaasahang pagkilos. + Bumalik + I-refresh + Ilapat + Sumulong + Webtoon + Internal na error: %s + SFW + NSFW + Uri ng nilalaman + Isang Hindi Inaasahang Error ang Naganap + Nagkaroon ng hindi inaasahang error ang %s. Iminumungkahi naming i-screenshot mo ang mensaheng ito, i-dump ang mga crash log, at pagkatapos ay ibahagi ito sa isang GitHub Issue. + I-refresh + Pahabang strip + Buksan ang mga extension / menu ng paglilipat + Ma-iwasan ang mga hadlang sa mahahabang pag-update ng aklatan, pag-download, at pag-restore ng mga backup. + Napili + Kung naglo-load ang reader ng isang blangkong imahe ay unti-unting bawasan ang threshold.\nNapili: %s + Walang napili + I-save ang mga pahina sa magkakaibang folder + Custom na profile sa display + Maaaring maayos ang isyu sa mga na-download na kabanata na magkasalungat sa isa\'t isa kapag pareho sila ng pangalan + Gumamit ng eksperimental na compose library + Mga naka-save na paghahanap + I-save ang pangalan ng paghahanap + I-save ang kasalukuyang query sa paghahanap? + Di-wastong naka-save na pangalan sa paghahanap + Tanggalin itong naka-save na query sa paghahanap? + Sigurado ka bang gusto mong tanggalin itong naka-save na query sa paghahanap: \'%1$s\'? diff --git a/i18n/src/commonMain/moko-resources/fr/plurals.xml b/i18n/src/commonMain/moko-resources/fr/plurals.xml index 537f5e5a77..d5486db060 100644 --- a/i18n/src/commonMain/moko-resources/fr/plurals.xml +++ b/i18n/src/commonMain/moko-resources/fr/plurals.xml @@ -1,78 +1,60 @@ - Mise à jour d\'extension disponible %d mises à jour des extensions disponibles %d mises à jour des extensions disponibles - et %1$d autre chapitre et %1$d autres chapitres et %1$d autres chapitres - Pour le titre du %d Pour les titres des %d Pour les titres des %d - Supprimer %1$d chapitre téléchargé \? Supprimer %1$d chapitres téléchargés \? Supprimer %1$d chapitres téléchargés \? - %d catégorie %d catégories %d catégories - - - %1$d page restante - %1$d pages restantes - %1$d pages restantes - - Après %1$s minute Après %1$s minutes Après %1$s minutes - Nettoyage terminé. %d dossier supprimé Nettoyage terminé. %d dossiers supprimés Nettoyage terminé. %d dossiers supprimés - Cache effacé. %d fichier a été supprimé Cache effacé. %d fichiers ont été supprimés Cache effacé. %d fichiers ont été supprimés - %d manga migré %d mangas migrés %d mangas migrés - Copier le manga %1$d%2$s \? Copier les mangas %1$d%2$s \? Copier les mangas %1$d%2$s \? - Migrer le manga %1$d%2$s \? Migrer les mangas %1$d%2$s \? Migrer les mangas %1$d%2$s \? - Un chapitre a été retiré de la source : \n%2$s @@ -86,70 +68,59 @@ \n \nSupprimer les téléchargements \? - Effectuée en %1$s avec %2$s erreur Effectuée en %1$s avec %2$s erreurs Effectuée en %1$s avec %2$s erreurs - %1$s chapitre %1$s chapitres %1$s chapitres - %1$d page %1$d pages %1$d pages - %d mise à jour en attente %d mises à jour en attente %d mises à jour en attente - Extension mise à jour %d extensions mises à jour %d extensions mises à jour - %d chapitre a été sauté, soit la source ne l\'a pas, soit il a été filtré %d chapitres ont été sautés, soit la source ne les a pas, soit ils ont été filtrés %d chapitres ont été sautés, soit la source ne les a pas, soit il ont été filtrés - Chapitre suivant non lu Les %d suivants non lus Les %d suivants non lus - %d type de série %d types de série %d types de série - %d source %d sources %d sources - %d état %d états %d états - %d langue %d langues %d langues - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/fr/strings.xml b/i18n/src/commonMain/moko-resources/fr/strings.xml index 920addd442..3f1c46fb2d 100644 --- a/i18n/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n/src/commonMain/moko-resources/fr/strings.xml @@ -740,10 +740,6 @@ Surfaces coupées du bloc Action d\'un appui long sur le filtre par Catégorie Tout annuler pour cette série - Pour télécharger des chapitres, TachiyomiJ2K nécessite l\'accès à tous les fichiers. Appuyez ici, puis activez « Autoriser l\'accès pour gérer tous les fichiers.» - Sous Android 11, TachiyomiJ2K nécessite l\'accès à tous les fichiers pour pouvoir télécharger des chapitres, créer des sauvegardes automatiques et lire des manga locaux. -\n -\nSur l\'écran suivant, activez « Autoriser l\'accès pour gérer tous les fichiers.» Inclure : %s Cela forcera le recalcul du cache de téléchargement. Utile si vous avez modifié des fichiers téléchargés en dehors de l\'application et que vous souhaitez que l\'application les récupère Rafraichir le cache de téléchargement @@ -767,7 +763,6 @@ Mode sombre noir pur Alignement de l\'icône de navigation latérale Fraises au chocolat - Autorisations de fichiers requises Sélectionner par défaut Par date de téléversement Non mis en favoris @@ -939,7 +934,6 @@ 5% Agents utilisateurs par défaut Certains langages peuvent nécessiter un redémarrage de l\'application pour s\'afficher correctement - Le format RARv5 n\'est pas supporté Téléchargement anticipé pendant la lecture Chapitres téléchargés Stat @@ -992,4 +986,97 @@ Entrées de la bibliothèque Informations de débogage Activité en arrière-plan + Autorisation de notification + Plus d\'Options + Naviguer vers le haut + Bienvenue! + Choisissons quelques valeurs par défaut. Vous pourrez toujours les modifier ultérieurement dans les paramètres. + Commencer + Sélectionnez un dossier dans lequel %1$s stockera les téléchargements de chapitres, les sauvegardes et plus encore.\n\nUn dossier dédié est recommandé.\n\nDossier sélectionné : %2$s + Sélectionner un dossier + Guide de stockage + Requis + Facultatif mais recommandé + Autorisation d\'installation d\'applications + Évitez les interruptions des mises à jour de bibliothèque, des téléchargements et des restaurations de sauvegarde de longue durée. + Accorder + Soyez informé des mises à jour de la bibliothèque et plus encore. + Utilisation de la batterie en arrière-plan + Aucun emplacement de stockage défini + Emplacement non valide: %s + Emplacement invalide + Vous effectuez une mise à jour à partir d\'une ancienne version et vous ne savez pas quoi sélectionner ? Reportez-vous à la section Mise à niveau Tachiyomi dans le guide de stockage Mihon pour plus d\'information. + Pour installer l\'application sur les mises à jour. + Montrer la queue de téléchargement + Rogner les bords (Bande longue) + Appui long Comportement naviguer + Défaut + Permet l\'installation d\'extensions sans invite utilisateur et active les mises à jour automatiques pour les appareils sous Android 12 + Révoquer toutes les extensions de confiance + Révoquer toutes les extensions de confiance ? + Page %1$d de %2$d + Non sélectionné + Sélectionné + Ouvre une série aléatoire (Global) + Ouvrir le dernier chapitre lu + Appui long Comportement récents + Des extensions malveillantes peuvent lire les identifiants de connexion enregistrés ou exécuter du code arbitraire.\n\nEn faisant confiance à cette extension, vous acceptez ces risques. + Bande longue + Ouvrir les paramètres de découpage hérité + Sur les appareils antérieurs à Android 9.0, vous devez définir les paramètres de découpe manuellement via les paramètres de votre système + Utiliser la bibliothèque de composition expérimentale + Aléatoire + Mises à Jour + Ouvrir le menu extensions / migration + Ouvrir la recherche globale + L\'installateur hérité n\'est pas encore implémenté, retour actuelle à PackageInstaller (par défaut) + Activer l\'action de balayage de chapitre + Par défaut (%d) + Appuyé deux fois pour zoomer + Utilisation du stockage + Hérité + Disponible : %1$s / Total : %2$s + Échec de l\'attribution d\'un dossier persistant. L\'appli peut se comporter de manière inattendu. + Dernière sauvegarde automatique : %s + Inclure les paramètre sensibles (ex. Tokens de connection au tracker) + Activer les actions de balayage pour les sources + Ajouter un répertoire + Remplacer + Empreinte de connection déjà existante + Déplacé la série vers le bas + Appuyé ici pour avoir de l\'aide avec Cloudflare + WebView est requis pour que l\'appli fonctionne + Retour + Avancer + Rafraîchir + Recherche sauvegardé + Sauvegardé l\'objet de recherche? + Sauvegardé le nom de la recherche + Nom de sauvegarde invalide + Supprimer cet objet de recherche sauvegardé ? + Partagé la couverture + Paramètre de l\'app + Paramètre des sources + Doki + Voir le contenu de la zone découpé + Créé un dossier selon le titre du manga + Sauvegardé les pages dans un dossiers à part + Profil d\'affichage personnalisé + Donnée et stockage + Dossier de stockage + Répertoires d\'extension + Ajouter un nouveau répertoire + URL du répertoire invalide + Supprimer le répertoire ? + Êtes-vous sûr de vouloir supprimer ce répertoire \"%s\"? + Répertoire déjà existant + Vous n\'avez pas encore ajouté de répertorié + Répertoire open source + Impossible d\'ouvrir l\'url + Ajouter automatiquement l\'ID + Peut corrigé des erreur de téléchargement avec des chapitre en conflit avec d\'autre ayant le même nom + Appliqué + Juste à l\'instant + Mode de débogage + Scanner les stockage externe pour des entrées diff --git a/i18n/src/commonMain/moko-resources/gl/strings.xml b/i18n/src/commonMain/moko-resources/gl/strings.xml index a549007f45..91188e0dd7 100644 --- a/i18n/src/commonMain/moko-resources/gl/strings.xml +++ b/i18n/src/commonMain/moko-resources/gl/strings.xml @@ -228,12 +228,7 @@ Capítulo %1$s Todos os capítulos lidos Scanlators - TachiyomiJ2K require acceso a tódolos arquivos de Android 11 para descargar capítulos, facer copias de seguridade automáticas e ler manga en local. -\n -\nNa seguinte pantalla, marca \"Permitir acceso á xestión de tódolos arquivos.\" - Requírense permisos de arquivo Non gardado nos marcadores - TachiyomiJ2K require acceso a tódolos arquivos para descargar capítulos, Preme eiquí, e despóis marca \"Permitir acceso á xestión de tódolos arquivos.\" Autor Move %1$s a… Selección inversa @@ -288,7 +283,6 @@ Borráronse as entradas Baixar capítulos novos Elexir a imaxe de portada - O formato RARv5 non está soportado Oimitiuse porque hai capítulos sin ler Actualizar o seguemento Erros @@ -494,4 +488,4 @@ Borrar o historial dos elementos que non estean gardados na túa biblioteca Información de depuración Actividade en segundo plano - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/hi/plurals.xml b/i18n/src/commonMain/moko-resources/hi/plurals.xml index 837756dda0..0d82bf18d0 100644 --- a/i18n/src/commonMain/moko-resources/hi/plurals.xml +++ b/i18n/src/commonMain/moko-resources/hi/plurals.xml @@ -1,53 +1,39 @@ - एक्सटेंशन अपडेट उपलब्ध%d एक्सटेंशन अपडेट उपलब्ध - %1$s में %2$s त्रुटि के साथ किया गया%1$s में %2$s त्रुटियों के साथ किया गया - %d श्रेणी %d श्रेणियाँ - %1$s मिनट के बाद%1$s मिनट के बाद - %d अध्याय को छोड़ा जा रहा है, या तो स्रोत में यह नहीं है या इसे फ़िल्टर कर दिया गया है %d अध्यायों को छोड़ा जा रहा है, या तो स्रोत उन्हें याद कर रहा है या उन्हें फ़िल्टर कर दिया गया है - अध्याय %1$s अध्यायों %1$s - - - %1$d पृष्ठ बाकि - %1$d पृष्ठ बाकि - - %1$d डाउनलोड अध्याय को निकालें\? %1$d डाउनलोड अध्यायों को निकालें\? - %d शीर्षक के लिए %d शीर्षकों के लिए - अगला अपठित अध्याय अगले %d अपठित अध्याय - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/hi/strings.xml b/i18n/src/commonMain/moko-resources/hi/strings.xml index 2c5688ea6d..356e81cd23 100644 --- a/i18n/src/commonMain/moko-resources/hi/strings.xml +++ b/i18n/src/commonMain/moko-resources/hi/strings.xml @@ -442,13 +442,8 @@ मानहुआ कॉमिक नए अध्याय - फाइल की अनुमति चाहिए - ताचियोमी को एंड्राइड ११ में फाइल अनुमति चाहिए ताकि वो चैप्टर डाउनलोड कर सके, आटोमेटिक बैकअप बना सके और लोकल माँगा पढ़ सके -\n -\nअगले स्क्रीन पर, सारे फाइल को एक्सेस करने की अनुमति को सेलेक्ट करे मानह्वा शुरू नहीं हुआ हैं - ताचियोमी ज २ क को सारे फाइल को एक्सेस करने की अनुमति चाहिए ताकि वो नए चैप्टर डाउनलोड कर सके. यहाँ पे क्लिक करके \" सारे फाइल को एक्सेस करने की अनुमति दे \" चालु हैं लाइब्रेरी तक पहुंचने के लिए अनलॉक करें अंतिम पढ़ा अध्याय %1$s @@ -545,7 +540,6 @@ ५% डाउनलोड किए गए अध्याय आपके फ़िल्टर के लिए कोई मिलान नहीं - RARv5 प्रारूप समर्थित नहीं है आगे डाउनलोड करें पढ़ते समय ऑटो डाउनलोड करे होल्ड लिस्ट में @@ -587,4 +581,7 @@ लाइब्रेरी के आइटम डीबग जानकारी पृष्ठभूमि गतिविधि + अधिक विकल्प + आइए कुछ डिफ़ॉल्ट चुनें। आप उन्हें बाद में सेटिंग में जाकर कभी भी बदल सकते हैं। + शुरू हो जाओ diff --git a/i18n/src/commonMain/moko-resources/hr/plurals.xml b/i18n/src/commonMain/moko-resources/hr/plurals.xml index 1a51514d75..41fdf79c9d 100644 --- a/i18n/src/commonMain/moko-resources/hr/plurals.xml +++ b/i18n/src/commonMain/moko-resources/hr/plurals.xml @@ -1,66 +1,50 @@ - %1$s poglavlje %1$s poglavlja %1$s poglavlja - Dostupna je %d nova verzija proširenja Dostupne su %d nove verzije proširenja Dostupoe je %d novih verzija proširenja - %d kategorija %d kategorije %d kategorija - - - %1$d preostala stranica - %1$d preostale stranice - %1$d preostalih stranica - - Ukloniti %1$d preuzeto poglavlje\? Ukloniti %1$d preuzeta poglavlja\? Ukloniti %1$d preuzetih poglavlja\? - i još %1$d naslov i još %1$d naslova i još %1$d naslova - Za %d naslov Za %d naslova Za %d naslova - Brisanje je gotovo. Uklonjena je %d mapa Brisanje je gotovo. Uklonjene su %d mape Brisanje je gotovo. Uklonjeno je %d mapa - Nakon %1$s minute Nakon %1$s minute Nakon %1$s minuta - Obavljeno u %1$s s %2$s greškom Obavljeno u %1$s s %2$s greške Obavljeno u %1$s s %2$s grešaka - Iz izvora je uklonjeno jedno poglavlje: \n%2$s @@ -72,82 +56,69 @@ \n%2$s \nŽeliš li izbrisati njihova preuzimanja\? - Predmemorija je izbrisana. %d datoteka je izbrisana Predmemorija je izbrisana. %d datoteke su izbrisane Predmemorija je izbrisana. %d datoteka je izbrisano - %d serija migrirana %d serije migrirane %d serija migrirano - Kopirati %1$d%2$s seriju\? Kopirati %1$d%2$s serije\? Kopirati %1$d%2$s serija\? - Migrati %1$d%2$s seriju\? Migrati %1$d%2$s serije\? Migrati %1$d%2$s serija\? - %1$d stranica %1$d stranice %1$d stranica - %d aktualiziranje na čekanju %d aktualiziranja na čekanju %d aktualiziranja na čekanju - %d proširenje aktualizirano %d proširenja aktualizirana %d proširenja aktualizirano - Preskače se %d poglavlje. Ne postoji u izvoru ili je filtrirano Preskaču se %d poglavlja. Ne postoje u izvoru ili su filtrirana Preskače se %d poglavlja. Ne postoje u izvoru ili su filtrirana - Sljedeće nepročitano poglavlje Sljedeća %d nepročitana poglavlja Sljedećih %d nepročitanih poglavlja - %d vrsta serije %d vrste serije %d vrsta serije - %d stanje %d stanja %d stanja - %d izvor %d izvora %d izvora - %d jezik %d jezika %d jezika - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/hr/strings.xml b/i18n/src/commonMain/moko-resources/hr/strings.xml index c1d6ed84f4..f4b8c28558 100644 --- a/i18n/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n/src/commonMain/moko-resources/hr/strings.xml @@ -706,10 +706,6 @@ Označi raspon poglavlja kao pročitana Prečaci programa Prekini sve za ovu seriju - TachiyomiJ2K zahtijeva pristup svim datotekama u Androidu 11 za preuzimanje poglavlja, za stvaranje automatskih sigurnosnih kopija i za čitanje lokalnih serija. -\n -\nU sljedećem ekranu aktiviraj „Dopusti pristup za upravljanje svim datotekama”. - Potrebne su dozvole za datoteke Globalna aktualiziranja Otvaranje slučajne serije Prikaži broj elemenata @@ -719,7 +715,6 @@ Po imenu izvora Po broju poglavlja Nije zabilježeno - TachiyomiJ2K zahtijeva pristup svim datotekama za preuzimanje poglavlja. Dodirni ovdje, zatim uključi „Dopusti pristup za upravljanje svim datotekama”. Neki proizvođači imaju dodatna programska ograničenja koja onemogućuju pozadinske usluge. Ova web-stranica sadrži daljnje informacije o tome kako to popraviti. Ovo će prisiliti ponovno izračunavanje predmemorije preuzimanja. Korisno, ako su preuzimanja promijenjena izvan ovog programa i ako želiš da ih program preuzme Upozorenje @@ -904,7 +899,6 @@ Otvori u programu 5 % Neki jezici mogu zahtijevati ponovno pokretanje aplikacije za ispravan prikaz - RARv5 format nije podržan Sve pročitane serije Standardni izraz korisničkog agenta Čuvaj unose s pročitanim poglavljima @@ -962,4 +956,4 @@ Postavke aplikacije Informacije otklanjanja grešaka Aktivnost u pozadini - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/hu/plurals.xml b/i18n/src/commonMain/moko-resources/hu/plurals.xml index 7888aabf78..1fa80616bc 100644 --- a/i18n/src/commonMain/moko-resources/hu/plurals.xml +++ b/i18n/src/commonMain/moko-resources/hu/plurals.xml @@ -1,58 +1,43 @@ - Egy új bővítményfrissítés érhető el%d bővítményfrissítés érhető el - %d kategória %d kategóriák - - - %1$d oldal hátra - %1$d oldalak hátra - - %1$s fejezet %1$s fejezetek - Eltávolítasz %1$d letöltött fejezetet\? Eltávolítasz %1$d letöltött fejezeteket\? - Befejezve %1$s alatt, %2$s hibával Befejezve %1$s alatt, %2$s hibával - 1 perc után%1$s percek után - %d-nak/nek%d-nak/nek - és még %1$d fejezet és még %1$d fejezet - Következő olvasatlan fejezet Következő %d olvasatlan fejezet - %d fejezet kihagyása, hiányzik a forrás, vagy ki lett szűrve %d fejezet kihagyása, hiányoznak a források, vagy ki lettek szűrve - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/hu/strings.xml b/i18n/src/commonMain/moko-resources/hu/strings.xml index 8f64bf7ce0..7a7cd51b75 100644 --- a/i18n/src/commonMain/moko-resources/hu/strings.xml +++ b/i18n/src/commonMain/moko-resources/hu/strings.xml @@ -504,13 +504,10 @@ Elkezdett Nem bejelentkezett trackerek: Művész - Fájl engedély szükséges - A TachiyomiJ2K a fejezetek letöltéséhez hozzáférést igényel az összes fájlhoz. Koppintson ide, majd engedélyezze a \"Hozzáférés engedélyezése az összes fájl kezeléséhez\" Az %1$s áthelyezése ide… %1$s hozzáadása a… Feloldás a könyvtárhoz való hozzáféréshez Felold - A TachiyomiJ2K a fejezetek letöltéséhez, automatikus biztossági mentés létrehozásához és a hely manga olvasásához hozzáférést igényel az összes fájlhoz. A következő képernyőn engedélyeze \"Hozzáférés engedélyezése az összes fájl kezeléséhez\" Nyelv jelvények Nincs elkezdve Nincs találat a jelenlegi filterre @@ -535,7 +532,6 @@ Sorozat típus 5% Alap hálózati kliens szöveg - RARv5 formátum nem támogatót Letölzozz fejezetek Kezdése gomb elrejtése Több könyvtár beállítás @@ -569,4 +565,4 @@ Könyvtár bejegyzések Debug információ Háttér aktivitás - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/in/plurals.xml b/i18n/src/commonMain/moko-resources/in/plurals.xml index 3f8834211e..0d01847b55 100644 --- a/i18n/src/commonMain/moko-resources/in/plurals.xml +++ b/i18n/src/commonMain/moko-resources/in/plurals.xml @@ -1,102 +1,75 @@ - Terdapat %d perbaharuan ekstensi - dan %1$d bab lain - Untuk %d judul - %d kategori - - - %1$d halaman tersisa - - Hapus %1$d bab yang diunduh\? - Selesai dalam %1$s dengan %2$s kesalahan - %1$s bab - %1$s bab telah dihapus dari sumber: \n%2$s \n \nHapus unduhan\? - %d manga dimigrasi - Salin manga %1$d%2$s\? - Pembersihan selesai. %d folder telah dihapus - Cache telah dibersihkan. %d berkas telah dihapus - %1$d halaman - Setelah %1$s menit - Migrasi %1$d%2$s manga\? - Pembaruan %d ditunda - %d - Melewati %d bab, entah sumbernya hilang atau telah difilter - Selanjutnya chapter %d yang belum dibaca - %d jenis seri - %d sumber - %d status - %d bahasa - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/in/strings.xml b/i18n/src/commonMain/moko-resources/in/strings.xml index 1a12821c17..1fa98db27f 100644 --- a/i18n/src/commonMain/moko-resources/in/strings.xml +++ b/i18n/src/commonMain/moko-resources/in/strings.xml @@ -706,7 +706,6 @@ Aktifkan %s Tidak ada Alternatif Ditemukan Tidak ada bab yang ditemukan, manga ini tidak dapat digunakan untuk migrasi - Perizinan berkas diperlukan Beberapa pabrikan mempunyai batasan aplikasi tambahan yang mematikan layanan latar belakang. Website ini memiliki info lebih lanjut untuk memperbaikinya. Hal ini akan memaksa cache unduhan untuk mengkalkulasi ulang. Berguna jika Anda memodifikasi unduhan di luar aplikasi ini dan ingin aplikasi ini untuk membacanya Beberapa ekstensi masih mengingatkan untuk dipasang terlebih dulu. @@ -757,10 +756,6 @@ Tampilkan jumlah item Pembaruan selesai Tak ditandai - TachiyomiJ2K membutuhkan akses semua berkas untuk mengunduh bab. Ketuk di sini, lalu izinkan \"Izinkan akses untuk mengatur semua berkas.\" - TachiyomiJ2K membutuhkan akses ke semua berkas di Android 11 untuk mengunduh bab, membuat cadangan otomatis, dan memuat manga lokal. -\n -\nPada layar selanjutnya, izinkan \"Izinkan akses untuk mengatur semua berkas\" Pembatasan: %1$s Hapus %1$s dari %2$s dan tambahkan %3$s Dipasang baru-baru ini @@ -905,7 +900,6 @@ 5% String agen pengguna default Beberapa bahasa mungkin memerlukan peluncuran ulang aplikasi untuk ditampilkan dengan benar - Format RARv5 tidak didukung Semua manga yang dibaca Simpan manga dengan membaca chapter Unduh otomatis ketika sedang membaca @@ -959,4 +953,30 @@ Entri pustaka Info debug Aktivitas dibelakang layar - + Selamat datang! + Opsi lebih lanjut + Segarkan + Sekarang + Tambah repo + Tambah repo baru + Pengaturan aplikasi + Doki + Penggunaan penyimpanan + Lokasi penyimpanan + Data dan penyimpanan + Tersedia: %1$s / Total: %2$s + Hapus repo? + Maju + Kembali + Segarkan + Tipe konten + Tidak dapat membuka url + Bawaan (%d) + Ganti + Pengaturan sumber + Bawaan + Mode debug + Url repo tidak valid + Masalah internal: %s + Acak + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/it/plurals.xml b/i18n/src/commonMain/moko-resources/it/plurals.xml index 377f9a40b8..286ab7078b 100644 --- a/i18n/src/commonMain/moko-resources/it/plurals.xml +++ b/i18n/src/commonMain/moko-resources/it/plurals.xml @@ -1,66 +1,50 @@ - Aggiornamento estensione disponibile %d estensioni hanno aggiornamenti disponibili %d estensioni hanno aggiornamenti disponibili - %d categoria %d categorie %d categorie - - - %1$d pagina rimasta - %1$d pagine rimaste - %1$d pagine rimaste - - Rimuovere %1$d capitolo scaricato\? Rimuovere %1$d capitoli scaricati\? Rimuovere %1$d capitoli scaricati\? - Dopo un minuto Dopo %1$s minuti Dopo %1$s minuti - Eliminazione completata. Rimossa la cartella %d Eliminazione completata. Rimosse le cartelle %d Eliminazione completata. Rimosse le cartelle %d - Cache libera. Il file %d è stato rimosso Cache libera. I file %d sono stati rimossi Cache libera. I file %d sono stati rimossi - Una serie migrata %d serie migrate %d serie migrate - Copiare la serie %1$d%2$s\? Copiare le serie %1$d%2$s\? Copiare le serie %1$d%2$s\? - Migrare la serie %2$s\? Migrare le serie %1$d%2$s\? Migrare le serie %1$d%2$s\? - Un capitolo è stato rimosso dalla sorgente: \n%2$s @@ -74,82 +58,69 @@ \n \nEliminare i loro download\? - e un altro capitolo e %1$d altri capitoli e %1$d altri capitoli - Per un titolo Per %d titoli Per %d titoli - Completato in %1$s con %2$s errore Completato in %1$s con %2$s errori Completato in %1$s con %2$s errori - %1$s capitolo %1$s capitoli - %1$d pagina %1$d pagine %1$d pagine - %d aggiornamento in attesa %d aggiornamenti in attesa %d aggiornamenti in attesa - Estensione aggiornata %d estensioni aggiornate %d estensioni aggiornate - %d capitolo saltato, la fonte non ce l\'ha o è stato filtrato %d capitoli saltati, la fonte non li ha o sono stati filtrati %d capitoli saltati, la fonte non li ha o sono stati filtrati - Il prossimo capitolo non letto I prossimi %d capitoli non letti I prossimi %d capitoli non letti - Un tipo di serie %d tipi di serie %d tipi di serie - Fonte %d fonti %d fonti - Status %d stati %d stati - Una lingua %d lingue %d lingue - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/it/strings.xml b/i18n/src/commonMain/moko-resources/it/strings.xml index 1240577768..454e1333bd 100644 --- a/i18n/src/commonMain/moko-resources/it/strings.xml +++ b/i18n/src/commonMain/moko-resources/it/strings.xml @@ -749,11 +749,6 @@ Nascondi badge non letti Apri una serie casuale Mostra il numero di oggetti - TachiyomiJ2K richiede accesso a tutti i file per scaricare i capitoli. Fai tap qui, poi fornisci l\'autorizzazione per \"Accesso a tutti i file.\" - TachiyomiJ2K richiede accesso ai file per scaricare capitoli, creare backup automatici e leggere serie in locale. -\n -\nAlla prossima schermata, fornisci l\'autorizzazione per \"Accesso a tutti i file.\" - Permessi per la gestione dei file richiesti Disattiva %s Attiva %s Selezione @@ -938,7 +933,6 @@ 5% Stringa user agent predefinita del browser Alcune lingue potrebbero richiedere un riavvio dell\'app per esserve visualizzate in maniera corretta - Il formato RARv5 non è supportato Tieni le voci con capitoli letti Tutte le serie lette Capitoli scaricati @@ -996,4 +990,4 @@ Attività in background Consente l\'installazione delle estensioni senza richieste da parte dell\'utente e abilita gli aggiornamenti automatici per i dispositivi con Android 12 Condividi copertina - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/iw/plurals.xml b/i18n/src/commonMain/moko-resources/iw/plurals.xml index 786dab276e..3302df72d6 100644 --- a/i18n/src/commonMain/moko-resources/iw/plurals.xml +++ b/i18n/src/commonMain/moko-resources/iw/plurals.xml @@ -1,73 +1,57 @@ - קטגורייה אחת שתי קטגוריות %d קטגוריות - זמין עדכון לתוסף זמינים עדכונים ל-%d תוספים זמינים עדכונים ל-%d תוספים - הושלם ב %1$s עם שגיאה אחת הושלם ב %1$s עם שתי שגיאות הושלם ב %1$s עם %2$s שגיאות - לאחר דקה אחת לאחר שתי דקות לאחר %1$s דקות - הסר %1$d פרק מההורדות\? הסר %1$d פרקים מההורדות\? הסר %1$d פרקים מההורדות\? הסר %1$d פרקים מההורדות\? - %1$s פרק %1$s פרקים %1$s פרקים %1$s פרקים - - - דף %1$d נותר - %1$d דפים נותרו - %1$d דפים נותרו - %1$d דפים נותרו - - עבור כותר אחד עבור שני כותרים עבור %d כותרים - דולג פרק אחד, המקור חסר או שהוא סונן החוצה דולגו שני פרקים, המקור חסר או שהם סוננו החוצה דולגו %d פרקים, המקור חסר או שהם סוננו החוצה - הפרק הבא שלא נקרא שני הפרקים הבאים שלא נקראו %d הפרקים הבאים שלא נקראו - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/iw/strings.xml b/i18n/src/commonMain/moko-resources/iw/strings.xml index eb364ef8be..a45df8318d 100644 --- a/i18n/src/commonMain/moko-resources/iw/strings.xml +++ b/i18n/src/commonMain/moko-resources/iw/strings.xml @@ -1,10 +1,5 @@ - נדרשת הרשאת גישה לקבצים - טאצ\'יומי J2K דורש גישה לכל הקבצים של אנדרואיד 11 כדי להוריד פרקים, צור גיבויים אוטומטיים, וקרא מנגה מקומית. -\n -\nבמסך הבא, אפשר \"תן גישה לכל הקבצים.\" - TachiyomiJ2K דורש גישה לכל הקבצים כדי להוריד פרקים. הקש כאן ולאחר מכן הפעל את \"אפשר גישה לניהול כל הקבצים.\" לעולם לא החדש ביותר הבא @@ -426,7 +421,6 @@ מדריך נדידת מקורות החרגה: %s %1$d עדכונים נכשלו - הפורמט RARv5 לא נתמך מוסיף %1$s לעדכון %1$s כבר נמצא בתור העדכון קטגוריה חדשה @@ -525,4 +519,4 @@ מחק את היסטוריית הפריטים שאינם שמורים בספריה שלך מידע דיבוג פעילות רקע - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ja/plurals.xml b/i18n/src/commonMain/moko-resources/ja/plurals.xml index dfbcdc245d..ee2c5859b5 100644 --- a/i18n/src/commonMain/moko-resources/ja/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ja/plurals.xml @@ -1,101 +1,74 @@ - %1$d件のダウンロードした章を削除してもよろしいですか? - %1$s章 - - - 残り%1$dページ - - %d カテゴリー - %d件のタイトル - とさらに%1$d章 - %d件のアップデートが待機中 - %d件の拡張機能をアップデートしました - %d件の拡張機能の更新が利用可能 - %1$dページ - ソースには存在しないか、フィルターによって排除されたため、%d章がスキップされました - 次の未読の%d章 - 第%1$s章はソースから消去されています: \n%2$s \nダウンロードを削除しますか? - %1$d%2$sのシリーズを移行しますか? - %1$d%2$sのシリーズをコピーしますか? - %dのシリーズを移行しました - %1$sで完成済み %2$s件のエラーが発生しました - キャッシュをクリアしました。%dファイルが削除されました - クリーンアップ完了。%dフォルダーが消去されました - %d件のシリーズ タイプ - %d件のソース - %d件のステータス - %d件の言語 - %1$s分後 - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ja/strings.xml b/i18n/src/commonMain/moko-resources/ja/strings.xml index 0f96aeb4e4..77adcbfdc4 100644 --- a/i18n/src/commonMain/moko-resources/ja/strings.xml +++ b/i18n/src/commonMain/moko-resources/ja/strings.xml @@ -1,10 +1,5 @@ - ファイルアクセス権限が必要 - 章のダウンロード、自動バックアップの作成及びローカルマンガシリーズの閲覧を行うには、TachiyomiJ2KはAndroid 11での全ファイルへのアクセス権が必要です。 -\n -\n次の画面で「全てのファイルの管理を許可」を有効にしてください。 - 章をダウンロードするには、TachiyomiJ2Kは全ファイルへのアクセス権が必要です。こちらをタップして「全てのファイルの管理を許可」を有効にしてください。 ようこそ! いくつかのデフォルトを選択しましょう。これらの設定は後から変更できます。 始める @@ -13,7 +8,7 @@ 旧バージョンからアップデートし、何を選ぶべきか分からない場合は、MihonストレージガイドのTachiyomi upgradeセクションを参照してください。 ストレージガイド 必須 - オプションですが、お勧めします。 + オプションですが、お勧めします アプリのインストール権限 ソース拡張機能をインストールするために必要です。 通知の権限 @@ -74,7 +69,6 @@ 並び順 章が見つかりません ページが見つかりません - フォーマットRARv5は未対応です 全てのダウンロードを削除しますか? 削除する章がありません ソースでの順位に基づく @@ -340,7 +334,7 @@ 読み終わりました: 読んでいます: 次: - 前: + 先: 次の章がありません 前の章がありません ページをロード中… @@ -453,7 +447,7 @@ カットアウト領域外から描画 カットアウト領域の動作は、特定のスケールタイプの縦向きモードでのみ適用されます レガシーカットアウト設定を開く - Android 9.0 より古いデバイスでは、システム設定から手動で設定する以外に、カットアウト設定を変更する方法はありません。 + Android 9.0 より古いデバイスでは、システム設定から手動で設定する以外に、カットアウト設定を変更する方法はありません カットアウト領域にコンテンツを表示 カットアウト領域を無視 ページレイアウト @@ -1075,4 +1069,10 @@ %s が予期しないエラーに遭遇しました。このメッセージをスクリーンショットにしてクラッシュログを保存し、GitHub Issue に共有することをお勧めします。 アプリを再起動 更新 + 無作為 + もっと設定 + ページ%1$dから%2$d + 不選択 + 選択 + 更新 diff --git a/i18n/src/commonMain/moko-resources/jv/strings.xml b/i18n/src/commonMain/moko-resources/jv/strings.xml index 2729483bfb..1b251f7c71 100644 --- a/i18n/src/commonMain/moko-resources/jv/strings.xml +++ b/i18n/src/commonMain/moko-resources/jv/strings.xml @@ -1,11 +1,6 @@ - Ijin berkas dibutuhake Manga - TachiyomiJ2K mbutuhake akses menyang kabeh file ing Android 11 kanggo ndownload bab, nggawe serep otomatis, lan maca manga lokal. -\n -\n Ing layar sabanjure, aktifake \"Allow akses kanggo ngatur kabeh file.\" - TachiyomiJ2K mbutuhake akses menyang kabeh file kanggo ngundhuh bab. Tutul ing kene, banjur aktifake \"Allow akses kanggo ngatur kabeh file.\" Manhwa Judhul Bab @@ -214,4 +209,4 @@ Awas Pemasang Entri perpustakaan - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ka/strings.xml b/i18n/src/commonMain/moko-resources/ka/strings.xml index 180dc383f5..52409233d1 100644 --- a/i18n/src/commonMain/moko-resources/ka/strings.xml +++ b/i18n/src/commonMain/moko-resources/ka/strings.xml @@ -320,8 +320,7 @@ გამოსახულებსი ჩატვირთვის შეცდომა მარქაფი უკვე მიმდინარეობს გაიგეთ, რატომ - ფაილის აუცილებელი წვდომები ნაცრისფერი ბიბლიოთეკის ჩანაწერები ისტორიიდან წაიშლება ჩანაწერები, რომლებიც თქვენს ბიბლიოთეკაში არაა შენახული - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/kk/plurals.xml b/i18n/src/commonMain/moko-resources/kk/plurals.xml index 0682967128..e143e74bbf 100644 --- a/i18n/src/commonMain/moko-resources/kk/plurals.xml +++ b/i18n/src/commonMain/moko-resources/kk/plurals.xml @@ -1,48 +1,35 @@ - %d санат %d санат - %1$s минуттан кейін%1$s минуттан кейін - %1$s дегеннен кейін %2$s қателікпен орындалды %1$s дегеннен кейін %2$s қателікпен орындалды - Келесі оқылмаған тарау Келесі %d оқылмаған тарау - %1$s тарау %1$s тарау - %1$d жүктелген тарауды жою керек пе\? %1$d жүктеген тарауды жою керек пе\? - - - %1$d бет қалды - %1$d бет қалды - - Кеңейту үшін жаңарту бар%d кеңейту үшін жаңарту бар - Дереккөзі жоқ немесе сүзілген %d тарау өткізіліп жіберілді Дереккөзі жоқ немесе сүзілген %d тарау өткізіліп жіберілді - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/kk/strings.xml b/i18n/src/commonMain/moko-resources/kk/strings.xml index 86bfede22b..a3641b2c1b 100644 --- a/i18n/src/commonMain/moko-resources/kk/strings.xml +++ b/i18n/src/commonMain/moko-resources/kk/strings.xml @@ -175,10 +175,6 @@ Сұрыптау Бастау Тоқтату - Файлдарға рұқсат керек - TachiyomiJ2K тарауларды жүктеп алу, автоматты сақтық көшірме жасау және жергілікті манга оқу үшін Android 11 жүйесіндегі барлық файлдарға кіруді қажет етеді. -\n -\nКелесі экранда «Барлық файлдарды басқаруға рұқсат беру» опциясын қосыңыз. Жолақты азайтады, бірақ өнімділікке әсер етуі мүмкін Өшірулі Соңғы оқылған тарау @@ -376,7 +372,6 @@ Күйі Файл таңдайтын қолданба табылмады Сурет сақталды - RARv5 пішімі қолжетімсіз %1$d жаңарту сәтсіз өтті Тарих жойылды Жоспарланған @@ -394,7 +389,6 @@ Мұқаба жаңартылды Кітапхана жаңаруда Жүктелген бет бөлінбеді - Тараулар жүктеу үшін TachiyomiJ2K-ге барлық файлдарды қарауға рұқсат беріңіз. Мынаны басып, \"Барлық файлдарды басқаруға рұқсат ету\" дегенді қосыңыз %1$s дегенді қайда жылжыту Барлық тараулар оқылды %2$d тараудың %1$d @@ -489,4 +483,4 @@ Кітапханада жоқ жазбалардың тарихын жою Дебаг туралы ақпарат Аялық белсенділік - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/km/plurals.xml b/i18n/src/commonMain/moko-resources/km/plurals.xml index 8cbe16f088..5349e5e618 100644 --- a/i18n/src/commonMain/moko-resources/km/plurals.xml +++ b/i18n/src/commonMain/moko-resources/km/plurals.xml @@ -1,23 +1,15 @@ - %d ថ្នាក់ - លុបភាគ %1$d ដែលបានទាញយកចោល\? - %1$s ភាគ - - - នៅសល់%1$d ទំព័រទៀត - - %d ភាគបន្ទាប់ដែលមិនទាន់អាន - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/km/strings.xml b/i18n/src/commonMain/moko-resources/km/strings.xml index 841aeefe13..f4f32ec1ca 100644 --- a/i18n/src/commonMain/moko-resources/km/strings.xml +++ b/i18n/src/commonMain/moko-resources/km/strings.xml @@ -118,10 +118,6 @@ ចែករំលែក ឈប់ខ្ទាស់ មើលភាគ - ត្រូវការការអនុញ្ញាត - តាឈិយ៉ូមិJ2K ត្រូវការaccessរាល់ហ្វល់ទាំងអស់នៅក្នុងអ៊េនដ្រយដ៏១១ដើម្បីទាញយកភាគ បង្កើតbackupsដោយស្វ័យប្រវត្ត និងអានម៊េងហ្គាដែលមានស្រាប់នៅក្នុងទូរសព្ទ។ -\n -\nនៅអេក្រង់បន្ទាប់​ បើក \"អនុញ្ញាតឲ្យaccessដើម្បីគ្រប់គ្រងរាល់ហ្វាល់ទាំងអស់\" ម៉ាន់វ៉ា ម៉ាន់ហួរ កូមិច @@ -130,7 +126,6 @@ វិចិត្រករ មិនទាន់ចាប់ផ្ដើម អាន - តាឈិយ៉ូមិJ2Kត្រូវការaccessរាល់ហ្វាល់ទាំងអស់ដើម្បីទាញយកភាគ ចុចត្រង់នេះបន្ទាប់មក \"អនុញ្ញាតឲ្យaccessដើម្បីគ្រប់គ្រងរាល់ហ្វាល់ទាំងអស់\" កំពុងដំណើរការ សង្ខេបរឿង ឡើង @@ -175,7 +170,6 @@ ទម្រង់ភាគមិនត្រឹមត្រួវ កំពុងតែធ្វើបច្ចុប្បន្នភាពបណ្ណាល័យ បានជ្រើសរើស: %1$d - ទម្រង់RARv5មិនត្រូវបានទទួលយកទេ គ្មានភាគដែលឲ្យលុបបានទេ ថ្នាក់ %1$s មាននៅក្នុងជួរ @@ -242,4 +236,4 @@ តម្រៀបតម្រងឡើងវិញ មាន %d មិនទាន់អាន មើលរឿងណាមួយ - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ko/plurals.xml b/i18n/src/commonMain/moko-resources/ko/plurals.xml index 1c5e70bd8d..eca63c90d8 100644 --- a/i18n/src/commonMain/moko-resources/ko/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ko/plurals.xml @@ -1,102 +1,75 @@ - 소요 시간: %1$s, 발생한 오류: %2$s - %1$s분 후 - %d 카테고리 - %d개의 확장 앱 업데이트가 있습니다 - %d개의 제목 - %1$s장 - 다운로드한 챕터 %1$d개를 제거하시겠습니까\? - - - %1$d페이지 남음 - - 그리고 %1$d개 화 남았다 - %d 확장 앱이 업데이트됨 - %d 업데이트 보류 중 - 읽지 않은 다음 %d 회차 - 소스에 존재하지 않거나 필터링되어 있는 %d개의 회차를 건너뛰었습니다 - %1$d 페이지 - %1$s 회차가 원본에서 제거되었습소스: \n%2$s \n \n다운로드를 삭제하시겠습니까\? - %d 만화가 마이그레이션됨 - %1$d%2$s 만화를 마이그레이션하시겠습니까\? - %1$d%2$s 만화를 복사하시겠습니까\? - 캐시가 지워졌습니다. %d 파일이 삭제되었습니다 - 정리가 완료되었습니다. 제거된 %d 폴더 - %d 언어 - "%d 시리즈 형식" - %d 소스 - %d 상태 - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ko/strings.xml b/i18n/src/commonMain/moko-resources/ko/strings.xml index 458b5e4257..780356ee7d 100644 --- a/i18n/src/commonMain/moko-resources/ko/strings.xml +++ b/i18n/src/commonMain/moko-resources/ko/strings.xml @@ -469,12 +469,6 @@ 읽지 않은 작품 진행 중 새 장 - 안드로이드의 파일 사용권한이 필요합니다 - 안드로이드 정책 변화에 따라 앱은 사용자에게 파일 접근권한을 요청합니다. -\nTachiyomiJ2K는 안드로이드11의 모든 파일에 접근하여, 챕터를 다운로드하고, 자동백업을 만들며, 휴대전화 저장소에 저장된 오프라인 만화를 읽을 수 있습니다. -\n -\n이에, 다음 화면에서 TachiyomiJ2K에 대한 \"모든 파일에 대한 접근권한\"을 허용해 주십시오. - TachiyomiJ2K는 챕터를 다운로드하기 위해 모든 파일에 액세스해야 합니다. 여기를 탭한 다음에 “모든 파일을 관리하기 위한 액세스 허용\"을 활성화해주십시오. 한국만화 중국만화 잠금 해제 @@ -617,7 +611,6 @@ 검색 팁이 주기적으로 표시됩니다. 추천 검색어를 길게 눌러 검색하세요. 5% 기본 사용자 에이전트 문자열 - RARv5 포맷은 지원되지 않습니다 이중 페이지 페이지 미리 로드 크기 유니폼 커버 @@ -960,4 +953,4 @@ 서재 항목 디버그 정보 백그라운드 활동 - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/lt/strings.xml b/i18n/src/commonMain/moko-resources/lt/strings.xml index 5808acad7e..89e298cc8f 100644 --- a/i18n/src/commonMain/moko-resources/lt/strings.xml +++ b/i18n/src/commonMain/moko-resources/lt/strings.xml @@ -289,7 +289,6 @@ Rūšiuoti pagal Skyrių nerasta Puslapių nerasta - RARv5 formatas nepalaikomas Visada rodyti skyrių perėjimus Šoninis spaudinėjimas Nėra @@ -474,4 +473,4 @@ Bibliotekos įrašai Ištrinkite įrašų istoriją, kurie nėra išsaugoti jūsų bibliotekoje Fono veikla - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/lv/plurals.xml b/i18n/src/commonMain/moko-resources/lv/plurals.xml index 5eaa15d1b0..36325015b4 100644 --- a/i18n/src/commonMain/moko-resources/lv/plurals.xml +++ b/i18n/src/commonMain/moko-resources/lv/plurals.xml @@ -1,63 +1,48 @@ - Pabeigts %1$s ar %2$s kļūdām Pabeigts %1$s ar %2$s kļūdu Pabeigts %1$s ar %2$s kļūdām - un vēl %1$d nodaļas un vēl %1$d nodaļa un vēl %1$d nodaļas - - - Palikušas %1$d lapas - Palikusi %1$d lapa - Palikušas %1$d lapas - - %1$s nodaļas %1$s nodaļa %1$s nodaļas - Pēc %1$s minūtēmPēc %1$s minūtesPēc %1$s minūtēm - %d kategoriju%d kategorija%d kategorijas - Pieejami %d paplašinājumi atjaunināšanaiPieejams %d paplašinājums atjaunināšanaiPieejami %d paplašinājumi atjaunināšanai - Priekš %dPriekš %dPriekš %d - Izlaisti %d nodaļu, vai nu to nav avotā, vai arī tie ir izfiltrēti Izlaists %d nodaļa, vai nu tā nav avotā, vai arī tā ir izfiltrēta Izlaistas %d nodaļas, vai nu tās nav avotā, vai arī tās ir izfiltrētas - Nākamā nelasītā nodaļa Nākamā nelasītā nodaļa Nākamās %d nelasītas nodaļas - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/lv/strings.xml b/i18n/src/commonMain/moko-resources/lv/strings.xml index 500a97f5aa..cd6db93e1c 100644 --- a/i18n/src/commonMain/moko-resources/lv/strings.xml +++ b/i18n/src/commonMain/moko-resources/lv/strings.xml @@ -166,8 +166,6 @@ Nosaukums Nav sākts Komikss - - Invertētas skāriena zonas Neviens Izsekots @@ -508,7 +506,6 @@ Bibliotēka pēdējo reizi atjaunināta: %s Lejupielādēt uz priekšu Logrīks nav pieejams, ja ir iespējota lietotņu bloķēšana - RARv5 formāts netiek atbalstīts Populārs Skatiet savus nesen atjauninātos bibliotēkas ierakstus Nederīga lietotāja agent virkne @@ -532,4 +529,4 @@ Dzēst vēsturi ierakstiem, kas nav saglabāti jūsu bibliotēkā Atkļūdošanas informācija Fona darbība - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/mn/strings.xml b/i18n/src/commonMain/moko-resources/mn/strings.xml index 2ce5cb2406..01178bd9d8 100644 --- a/i18n/src/commonMain/moko-resources/mn/strings.xml +++ b/i18n/src/commonMain/moko-resources/mn/strings.xml @@ -1,12 +1,7 @@ - Файлийн зөвшөөрөл шаардлагатай Комик - \"TachiyomiJ2K\" нь Андройд 11-ийн бүх файлыг хэрэглэх зөвшөөрөл авч байж утсан дээрээс тань манга уншиж, манга татах боломжтой. -\n -\nДараа гарч ирэх хэсэгт, бүх файлийг удирдахыг зөвшөөрнө үү. Эхлээгүй - TachiyomiJ2K бүх файлуудыг ашиглах зөвшөөрөлтэй байж манга татах боломжтой. Энд дараад, бүх файлыг удирдах зөвшөөрөл олгоно уу. Манхуа Манхуа Ашиглахын тулд онгойлгоно уу @@ -45,7 +40,6 @@ Бүлгийг устгалаа. Бүлэг олдсонгүй Ямар ч бүлэг олдсонгүй - \"RARv5\" формат дэмжихгүй Бүлгийн бичиглэл буруу Эрэмбэлэх Хуудас олдсонгүй diff --git a/i18n/src/commonMain/moko-resources/ms/plurals.xml b/i18n/src/commonMain/moko-resources/ms/plurals.xml index f76c53e464..e5af82913e 100644 --- a/i18n/src/commonMain/moko-resources/ms/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ms/plurals.xml @@ -1,101 +1,74 @@ - %d kemas kini sambungan tersedia - dan %1$d lebih - Untuk %d - - - %1$d halaman yang tinggal - - Buang %1$d bab yang dimuat turun\? - Selepas %1$s - Pembersihan selesai. Di buang %d folder - Chache di bersihkan. %d fail telah di - %d manga di - Salin %1$d%2$s manga\? - Pindah %1$d%2$s manga\? - %1$s bab telah dibuang daripada sumber: \n%2$s \nPadam muat turun\? - %d kategori - Selesai dalam %1$s dengan %2$s ralat - %1$s bab - %1$d halaman - Melangkau %d bab, sama ada sumber tidak mempunyai bab tersebut, atau ia ditapis keluar - %d kemas kini belum selesai - %d sambungan dikemas kini - %d bab tidak dibaca seterusnya - %d jenis siri - %d sumber - %d status - %d bahasa - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ms/strings.xml b/i18n/src/commonMain/moko-resources/ms/strings.xml index 3e709d78c5..4b375306e2 100644 --- a/i18n/src/commonMain/moko-resources/ms/strings.xml +++ b/i18n/src/commonMain/moko-resources/ms/strings.xml @@ -706,10 +706,6 @@ Pintasan apl Tandakan julat bab sebagai belum dibaca Tandakan julat bab sebagai dibaca - TachiyomiJ2K memerlukan akses kepada semua fail di Android 11 untuk memuat turun bab, membuat sandaran automatik, dan membaca manga lokal. -\n -\nPada skrin berikutnya, hidupkan \"benarkan akses mengurus semua fail.\" - Kebenaran fail diperlukan Sumber tidak disokong Sembunyikan kandungan pemberitahuan Sesetengah pengeluar ada sekatan tambahan pada aplikasi yang akan menghentikan perkidmatan latar belakang. Laman web ini ada maklumat cara membaikinya. @@ -805,7 +801,6 @@ Zon ketik Mengikut urutan sumber Lambang bahasa - TachiyomiJ2K memerlukan akses kepada semua fail untuk memuat turun bab. Tap sini, kemudian hidupkan \"benarkan akses untuk mengurus semua fail.\" Papar garisan luar sekeliling muka hadapan Versi beta baharu tersedia! scanlator @@ -816,7 +811,6 @@ kemas kini siap 5% Untaian ejen pengguna lalai - Format RARv5 tidak disokong Tidak ditanda buku Papar %1$s Tambah label @@ -950,4 +944,4 @@ Entri pustaka Maklumat nyahpepijat Aktiviti latar belakang - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/my/plurals.xml b/i18n/src/commonMain/moko-resources/my/plurals.xml index ec069b8248..d1f94ec90e 100644 --- a/i18n/src/commonMain/moko-resources/my/plurals.xml +++ b/i18n/src/commonMain/moko-resources/my/plurals.xml @@ -1,19 +1,12 @@ - အမျိုးအစား %d ခု - - - စာမျက်နှာ %1$d ခုကျန်သေးသည် - - %1$s ပိုင်း - ဒေါင်းလုဒ်ဆွဲထားတဲ့အပိုင်း %1$d ပိုင်းကိုဖျက်ပစ်မှာလား - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/my/strings.xml b/i18n/src/commonMain/moko-resources/my/strings.xml index 809228610c..a22ace7ad5 100644 --- a/i18n/src/commonMain/moko-resources/my/strings.xml +++ b/i18n/src/commonMain/moko-resources/my/strings.xml @@ -153,7 +153,6 @@ ပြောင်းပြန်ရွေးချယ်ရန် အရံ ဒေါင်းလုဒ်အမှတ်အသားများ - ဖိုင်ခွင့်ပြုချက်လိုအပ်သည် အသစ်ထည့်ထားသော ဖျက်စရာအပိုင်းမရှိပါ ဒေါင်းထားတာတွေအားလုံးကို ဖျက်မှာလား\? @@ -173,9 +172,6 @@ … အဖြစ် ပြမည် အမျိုးအစားခုန်ကျော်သည့် ခလုတ်ကို ဖျောက်မည် နောက်ထပ် Library ဆက်တင်များ - Android 11 တွင် အပိုင်းများဒေါင်းလုဒ်လုပ်ရန်၊ အလိုအလျောက် backup လုပ်ရန်နှင့် စက်တွင်း Manga များဖတ်ရန် TachiyomiJ2K အား စက်တွင်းဖိုင်အားလုံးရယူခွင့်ပေးဖို့ လိုအပ်ပါသည်။ -\n -\nနောက်စခရင်သို့ ရောက်ရှိပါက \"ဖိုင်အားလုံးစီမံပိုင်ခွင့်\" ကို ဖွင့်ပေးပါ။ အင်တာနက်သုံး၍ အပ်ဒိတ်လုပ်ခဲ့သော ရက်စွဲအရ လေးထောင့်ကွက်ဆိုဒ် အပိုင်းအသစ်များထွက်ရှိထားပါသည် @@ -187,8 +183,6 @@ စတင်ခြင်းလမ်းညွှန် မမှန်ကန်သော အပိုင်းပုံစံဖြစ်နေသည် Filter လုပ်နေစဥ်တွင် ဗလာဖြစ်နေသည့်အမျိုးအစားများပါ ပြပေးပါ - အပိုင်းများဒေါင်းလုဒ်ဆွဲဖို့အတွက် TachiyomiJ2K အား ဖိုင်အားလုံးရယူခွင့်ပေးဖို့ လိုအပ်ပါသည်။ ဤနေရာကိုနှိပ်ပြီး \"ဖိုင်အားလုံးစီမံပိုင်ခွင့်\" ကို ဖွင့်ပေးပါ။ - RARv5 format ကို အထောက်အပံ့မပေးထားပါ သင့် filter နှင့် ကိုက်ညီသောရလဒ်မတွေ့ရှိပါ Beta ဗားရှင်းအသစ် ထွက်နေပါပြီ! အပ်ဒိတ်ကို သွင်းလို့မရခဲ့ပါ @@ -219,4 +213,4 @@ သက်တောင့်သက်သာရှိသော လေးထောင့်ပုံစံ ကျစ်ကျစ်လစ်လစ်ရှိသော လေးထောင့်ပုံစံ အပ်ဒိတ်အများအပြားလုပ်ခြင်းအားဖြင့် ဘက်ထရီသုံးစွဲမှု ပိုမိုများပြားစေသည့်အပြင် ရင်းမြစ်များကြည့်ရှုရာတွင်လည်း ပိုမိုနှေးကွေးစေနိုင်ပါသည်။ ပိုမိုသိရှိရန် ဤနေရာကိုနှိပ်ပါ။ - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml b/i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml index 3ff93df564..8672920ff1 100644 --- a/i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml +++ b/i18n/src/commonMain/moko-resources/nb-rNO/plurals.xml @@ -1,46 +1,33 @@ - Utvidelsesoppdatering tilgjengelig %d utvidelsesoppdateringer tilgjengelige - Gjort på %1$s med %2$s feil Gjort på %1$s med %2$s feil - %d kategori %d kateogrier - Fjern %1$d nedlastet kapittel\? Fjern %1$d nedlastede kapitler\? - Hopper over %d kapittel, enten så mangler kilden den eller så har den blitt filtrert ut Hopper over %d kapitler, enten så mangler kilden de eller så har de blitt filtrert ut - For én tittel For %d titler - - - %1$d side igjen - %1$d sider igjen - - %1$s kapittel %1$s kapitler - kapittel har blitt fjernet fra denne kilden \n%2$s @@ -51,79 +38,64 @@ \n \nSlett nedlastningenene av dem\? - Opprenskning ferdig. Fjernet én mappe Opprenskning ferdig. Fjernet %d mapper - og %1$d kapittel til og %1$d kapitler til - Hurtiglager tømt. %d fil har blitt slettet. Hurtiglager tømt. %d filer har blitt slettet. - Etter %1$s minutt Etter %1$s minutter - %d manga flyttet %d manga flyttet - Kopier %1$d%2$s manga\? Kopier %1$d%2$s manga\? - Flytt %1$d%2$s manga\? Flytt %1$d%2$s manga\? - %1$d side %1$d sider - Utvidelse oppdatert %d utvidelser oppdatert - %d oppdatering venter %d oppdateringer venter - Neste uleste kapittel Neste %d uleste kapitler - %d språk %d språk - %d serietype %d serietyper - %d status %d statuser - %d kilde %d kilder - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/nb-rNO/strings.xml b/i18n/src/commonMain/moko-resources/nb-rNO/strings.xml index 845c370d97..38b53fb2ba 100644 --- a/i18n/src/commonMain/moko-resources/nb-rNO/strings.xml +++ b/i18n/src/commonMain/moko-resources/nb-rNO/strings.xml @@ -445,7 +445,6 @@ Legg til %1$s i … Underveis Ikke startet - Filtilganger kreves Navn Ulest Avskrudd @@ -717,10 +716,6 @@ Åpne en tilfeldig serie Manhua Manhwa - TachiyomiJ2K krever tilgang til alle filer for å laste ned kapitler. Trykk her og skru på «Tillat tilgang til håndtering av alle filer.» - TachiyomiJ2K krever tilgang til alle filer i Android 11 for å laste ned kapitler, opprette automatiske sikkerhetskopier, og lese lokal manga. -\n -\nPå neste skjerm skrur du på «Innvilg tilgang til håndtering av alle filer.» Safirskumring Slett under global oppdatering, spør på kapittelsiden Fjern foreldreløse @@ -871,7 +866,6 @@ Last ned automatisk mens du leser Kunne ikke dele det nedlastede bildet Side %d ble ikke funnet under deling - RARv5-formatet støttes ikke Last ned i forkant Fungerer kun på oppføringer i biblioteket, og hvis det nåværende kapittelet samt det neste allerede er lastet ned Del opp høye bilder @@ -961,4 +955,4 @@ Programinnstillinger Debug info Bakgrunnsaktivitet - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ne/plurals.xml b/i18n/src/commonMain/moko-resources/ne/plurals.xml index a4e8ee3263..458ba1222d 100644 --- a/i18n/src/commonMain/moko-resources/ne/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ne/plurals.xml @@ -1,91 +1,69 @@ - %d वर्ग %d वर्गहरू - %1$s अध्याय %1$s अध्यायहरू - एक्सटेन्शन अपडेट उपलब्ध छ %d एक्सटेन्शन अपडेटहरू उपलब्ध छन् - अर्को नपढिएको अध्याय अर्को %d नपढिएका अध्यायहरू - %2$s त्रुटिको साथ %1$s मा सम्पन्न भयो %2$s त्रुटिहरूसँग %1$s मा सम्पन्न भयो - %1$s मिनेट पछि %1$s मिनेट पछि - %d अध्याय छोड्दै, या त स्रोत सँग छैन वा यसलाई फिल्टर गरिएको छ %d अध्यायहरू छोड्दै, या त स्रोत सँग छैन वा यसलाई फिल्टर गरिएको छ - श्रृङ्खला प्रकार %d श्रृङ्खला प्रकारहरू - स्रोत %d स्रोतहरू - भाषा %d भाषाहरू - - - %1$d पृष्ठ बाँकी छ - %1$d पृष्ठहरू बाँकी छन् - - %1$d डाउनलोड गरिएको अध्याय हटाउनुहुन्छ\? %1$d डाउनलोड गरिएका अध्यायहरू हटाउनुहुन्छ\? - %d अपडेट विचाराधीन %d अपडेटहरू विचाराधीन छन् - %d शीर्षकको लागि %d शीर्षकहरूको लागि - र थप %1$d अध्याय र थप %1$d अध्यायहरू - एक्सटेन्शन अपडेट गरियो %d एक्सटेन्शनहरू अपडेट गरियो - %1$d पृष्ठ %1$d पृष्ठहरू - स्रोतबाट एउटा अध्याय हटाइयो: \n%2$s @@ -95,34 +73,28 @@ \n \nतिनीहरूका डाउनलोडहरू हटाउने हो\? - %d श्रृङ्खला स्थानान्तरण भयो %d श्रृङ्खला स्थानान्तरण भयो - %1$d%2$s श्रृङ्खला प्रतिलिपि गर्ने हो\? %1$d%2$s श्रृङ्खला प्रतिलिपि गर्ने हो\? - %1$d%2$s श्रृङ्खला स्थानान्तरण गर्ने हो\? %1$d%2$s श्रृङ्खला स्थानान्तरण गर्ने हो\? - सफाई सक्यो। %d फोल्डर हटाइयो सफाई सक्यो। %d फोल्डरहरू हटाइयो - स्थिति %d स्थितिहरू - क्यास खाली गरियो। %d फाइल मेटाइएको छ क्यास खाली गरियो। %d फाइलहरू मेटिएका छन् - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ne/strings.xml b/i18n/src/commonMain/moko-resources/ne/strings.xml index 1481cd0dd2..99ef617e9c 100644 --- a/i18n/src/commonMain/moko-resources/ne/strings.xml +++ b/i18n/src/commonMain/moko-resources/ne/strings.xml @@ -75,7 +75,6 @@ अवैध अध्याय ढाँचा द्वारा अर्डर गर्नुहोस् अध्यायहरू भेटिएन - RARv5 समर्थित छैन अध्याय संख्या द्वारा अपलोड मिति द्वारा वर्गहरू @@ -420,8 +419,6 @@ अज्ञात त्रुटि अनपिन अध्यायहरू हेर्नुहोस् - TachiyomiJ2K लाई अध्यायहरू डाउनलोड गर्न सबै फाइलहरू माथि पहुँच चाहिन्छ। यहाँ ट्याप गर्नुहोस्, त्यसपछि \"Allow access to manage all files.\" सक्षम गर्नुहोस्। - फाइल अनुमति आवश्यक छ माङ्गा मानह्वा मानहुवा @@ -448,9 +445,6 @@ अनलक गर्न आवश्यक छ सबै डाउनलोडहरू हटाउने हो\? ब्याकअप पहिले नै प्रगतिमा छ - TachiyomiJ2K लाई अध्यायहरू डाउनलोड गर्न, स्वचालित ब्याकअपहरू सिर्जना गर्न र लोकल श्रृङ्खला पढ्नको लागि Android 11 मा सबै फाइलहरू माथि पहुँच चाहिन्छ। -\n -\nअर्को स्क्रिनमा, \"Allow access to manage all files.\" सक्षम गर्नुहोस्। ट्र्याकर पुस्तकालयमा शीर्षकहरू ट्र्याक गरिएका शीर्षकहरू @@ -962,4 +956,4 @@ एप सेटिङहरू Debug जानकारी ब्याकग्राउण्ड गतिविधि - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/nl/plurals.xml b/i18n/src/commonMain/moko-resources/nl/plurals.xml index fca879e264..712674dd35 100644 --- a/i18n/src/commonMain/moko-resources/nl/plurals.xml +++ b/i18n/src/commonMain/moko-resources/nl/plurals.xml @@ -1,66 +1,49 @@ - Extensie-update beschikbaar %d extensie-updates beschikbaar - Na %1$s minuut Na %1$s minuten - en %1$d nog hoofdstuk en %1$d nog hoofdstukken - Voor %d titel Voor %d titels - %d categorie %d categorieën - - - %1$d pagina over - %1$d paginas over - - Verwijder %1$d gedownload hoofdstuk\? Verwijder %1$d gedownloade hoofdstukken\? - Schoonmaak klaar. %d map verwijderd Schoonmaak klaar. %d mappen verwijderd - Cache geleegd. %d bestand is verwijderd Cache geleegd. %d bestanden zijn verwijderd - %d manga gemigreerd %d manga gemigreerd - %1$d%2$s manga kopiëren\? %1$d%2$s manga kopiëren\? - %1$d%2$s manga migreren\? %1$d%2$s manga migreren\? - Een hoofdstuk is verwijderd uit de bron: \n%2$s @@ -70,59 +53,48 @@ \n \nDownloads van deze hoofdstukken verwijderen\? - Voltooid in %1$s met %2$s foutmelding Voltooid in %1$s met %2$s foutmeldingen - %1$s hoofdstuk %1$s hoofdstukken - %1$d pagina %1$d pagina\'s - %d wachtende update %d wachtende updates - Extensie bijgewerkt %d extensies bijgewerkt - 1 hoofdstuk is overgeslagen, de bron mist 1 hoofdstuk of het is uitgefilterd %d hoofdstukken zijn overgeslagen, de bron mist ze of ze zijn uitgefilterd - Volgende ongelezen hoofdstuk Volgende aantal %d ongelezen hoofdstukken - %d serie types - %d bronnen - %d statussen - %d talen - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/nl/strings.xml b/i18n/src/commonMain/moko-resources/nl/strings.xml index 12acadcaff..759326871a 100644 --- a/i18n/src/commonMain/moko-resources/nl/strings.xml +++ b/i18n/src/commonMain/moko-resources/nl/strings.xml @@ -706,9 +706,6 @@ Markeer een reeks hoofstukken als gelezen Alles annuleren voor deze serie App-snelkoppelingen - TachiyomiJ2K heeft toegang nodig tot alle bestanden in Android 11 om hoofdstukken te downloaden, automatische backups aan te maken, en lokale manga te lezen. -\n -\nOp het volgende scherm, schakel \"Toegang verlenen om alle bestanden te beheren\" in. Oriëntatie Standaardoriëntatie Inbegrepen: %s @@ -742,8 +739,6 @@ Op hoofdstuknummer Op volgorde van de bron Geen bladwijzer - TachiyomiJ2K heeft toegang nodig tot alle bestanden om hoofdstukken te downloaden. Tik hier, en zet dan \"Toegang verlenen om alle bestanden te beheren.\" aan. - Bestandsrechten vereist Backup/herstellen werkt waarschijnlijk niet goed als de MIUI Optimalisatie is uitgeschakeld. Alleen via Wi-Fi Tako @@ -883,7 +878,6 @@ Lavender Voor sommige talen moet de app opnieuw worden gestart om correct weer te geven Download vooruit - RARv5-indeling wordt niet ondersteund Tik op zones Violet Standaard user agent string @@ -958,4 +952,4 @@ Bibliotheek Foutopsporingsinformatie Achtergrond activiteit - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/om/plurals.xml b/i18n/src/commonMain/moko-resources/om/plurals.xml index 98ccfd6a22..49b7188649 100644 --- a/i18n/src/commonMain/moko-resources/om/plurals.xml +++ b/i18n/src/commonMain/moko-resources/om/plurals.xml @@ -1,23 +1,15 @@ - Boqonnaa %1$d buufame haquu\? Boqonnaawwan %1$d buufaman haquu\? - Boqonnaa %1$s Boqonnaawwan %1$s - - - fuula %1$d hafe - fuulootaa %1$d hafe - - Ramaddi %d Ramaddiiwwan %d - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/om/strings.xml b/i18n/src/commonMain/moko-resources/om/strings.xml index 1c04eca211..1a19b8540b 100644 --- a/i18n/src/commonMain/moko-resources/om/strings.xml +++ b/i18n/src/commonMain/moko-resources/om/strings.xml @@ -1,6 +1,5 @@ - Hayyama faayilii barbaachisa Mangaa Hin jalqabne Adeemsa irra jira @@ -37,11 +36,7 @@ Guyyaa olkaa\'ameen Ramaddii Ramaddiiwwan - TachiyomiJ2K boqonnaawwan buusuuf, ofumaan dilbeessa uumuu fi mangaa naannoo dubbisuuf, faayiloota Android 11 keessa jiran hunda argachuu barbaachisa. -\n -\n Foddaa itti aanu irratti, \"Allow access to manage all files.\" Kan jedhuuf eeyyami . Baacoo - TachiyomiJ2K boqonnaawwan buusuuf faayiloota hunda argachuu barbaada. As tuqi, sana booda \"Allow access to manage all files\" kan jedhuuf eeyyami. Barreessaa Boqonnaawwan haaraa Maxxansi xumurame @@ -75,4 +70,4 @@ Dogoggora hin beekamne Ramaddiiwwan gulaali Ramaddii bulchuu - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pl/plurals.xml b/i18n/src/commonMain/moko-resources/pl/plurals.xml index 986a28ebe4..86bc50d7c0 100644 --- a/i18n/src/commonMain/moko-resources/pl/plurals.xml +++ b/i18n/src/commonMain/moko-resources/pl/plurals.xml @@ -1,55 +1,41 @@ - Usunąć %1$d pobrany rozdział? Usunąć %1$d pobrane rozdziały? Usunąć %1$d pobranych rozdziałów? Usunąć %1$d pobranych rozdziałów? - %1$s rozdział %1$s rozdziały %1$s rozdziałów %1$s rozdziałów - - - Pozostała %1$d strona - Pozostały %1$d strony - Pozostało %1$d stron - Pozostało %1$d stron - - %d kategoria %d kategorii %d kategorii %d kategorii - Dla %d tytułu Dla %d tytułów Dla %d tytułów Dla %d tytułów - i %1$d rozdział więcej i %1$d rozdziały więcej i %1$d rozdziałów więcej i %1$d rozdziałów więcej - Aktualizacja rozszerzenia dostępna %d aktualizacje rozszerzeń dostępne %d aktualizacji rozszerzeń dostępnych %d aktualizacji rozszerzeń dostępnych - Rozdział został usunięty ze źródła: \n%2$s @@ -67,116 +53,100 @@ \n \nUsunąć pobrane\? - Migrować %1$d%2$s mangę? Migrować %1$d%2$s mangi? Migrować %1$d%2$s mang? Migrate %1$d%2$s mang? - Skopiować %1$d%2$s mangę? Skopiować %1$d%2$s mangi? Skopiować %1$d%2$s mang? Skopiować %1$d%2$s mang? - %d manga zmigrowana %d mangi zmigrowane %d mang zmigrowanych %d mang zmigrowanych - Pamięć podręczna wyczyszczona. %d plik został usunięty Pamięć podręczna wyczyszczona. %d pliki zostały usunięty Pamięć podręczna wyczyszczona. %d plików zostało usuniętych Pamięć podręczna wyczyszczona. %d plików zostało usuniętych - Czyszczenie skończone. Usunięto %d folder Czyszczenie skończone. Usunięto %d foldery Czyszczenie skończone. Usunięto %d folderów Czyszczenie skończone. Usunięto %d folderów - Po %1$s minucie Po %1$s minutach Po %1$s minutach Po %1$s minutach - Wykonano w %1$s z %2$s błędem Wykonano w %1$s z %2$s błędami Wykonano w %1$s z %2$s błędami Wykonano w %1$s z %2$s błędami - %1$d strona %1$d strony %1$d stron %1$d stron - Rozszerzenie zaktualizowane %d rozszerzenia zaktualizowane %d rozszerzeń zaktualizowanych %d rozszerzeń zaktualizowanych - %d oczekująca aktualizacja %d oczekujące aktualizacje %d oczekujących aktualizacji %d oczekujących aktualizacji - %d brakujący rozdział %d brakujące rozdziały %d brakujących rozdziałów %d brakujących rozdziałów - Następny nieprzeczytany rozdział Następne %d nieprzeczytane rozdziały Następne %d nieprzeczytanych rozdziałów Następne %d nieprzeczytanych rozdziałów - %d źródło %d źródła %d źródeł %d źródeł - %d status %d statusy %d statusów %d statusów - %d język %d języki %d języków %d języków - %d typ serii %d typów serii %d typów serii %d typów serii - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pl/strings.xml b/i18n/src/commonMain/moko-resources/pl/strings.xml index 9ec922660a..979a0f189d 100644 --- a/i18n/src/commonMain/moko-resources/pl/strings.xml +++ b/i18n/src/commonMain/moko-resources/pl/strings.xml @@ -716,11 +716,6 @@ Źródło nie jest wspierane Funkcje kopii zapasowej mogą nie działać, jeśli optymalizacja nakładki MIUI jest wyłączona. Ukryj zawartość powiadomienia - TachiyomiJ2K wymaga dostępu do wszystkich plików w celu pobrania rozdziałów. Dotknij tutaj, a następnie włącz \"Zezwól na dostęp do wszystkich plików.\" - TachiyomiJ2K wymaga dostępu do wszystkich plików w Android 11 w celu pobierania rozdziałów, tworzenia automatycznych backupów oraz czytania lokalnie zapisanych mang. -\n -\nNa następnym ekranie włącz opcję \"Zezwól na dostęp do wszystkich plików.\" - Wymagany jest dostęp do plików Podgląd Ostatnio zainstalowane Ostatnio zaktualizowane @@ -929,7 +924,6 @@ Otwórz w aplikacji 5% Domyślny user agent string - Format RARv5 jest nieobsługiwany Poziom baterii nie jest niski Wersje beta mogą być niestabilne i mogą wymagać wyczyszczenia danych aplikacji. Fiołkowy @@ -996,4 +990,4 @@ Biblioteka Informacje debugowania Aktywność w tle - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml b/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml index 27d71c4da8..0ea1b27969 100644 --- a/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml +++ b/i18n/src/commonMain/moko-resources/pt-rBR/plurals.xml @@ -1,48 +1,35 @@ - Remover %1$d capítulo baixado\? Remover %1$d capítulos baixados\? Remover %1$d capítulos baixados\? - %1$s capítulo %1$s capítulos %1$s capítulos - - - %1$d página restante - %1$d páginas restantes - %1$d páginas restantes - - %d categoria %d categorias %d categorias - Para %d título Para %d títulos Para %d títulos - e mais %1$d capítulo e mais %1$d capítulos e mais %1$d capítulos - Atualização de extensão disponível %d atualizações de extensão disponíveis %d atualizações de extensão disponíveis - Um capítulo foi removido da fonte: \n%2$s @@ -56,100 +43,84 @@ \n \nExcluir seus downloads\? - Migrar %1$d%2$s mangá\? Migrar %1$d%2$s mangás\? Migrar %1$d%2$s mangás\? - Copiar %1$d%2$s mangá\? Copiar %1$d%2$s mangás\? Copiar %1$d%2$s mangás\? - %d mangá migrado %d mangás migrados %d mangás migrados - Cache limpo. %d arquivo foi excluído Cache limpo. %d arquivos foram excluídos Cache limpo. %d arquivos foram excluídos - Limpeza realizada. %d pasta removida Limpeza realizada. %d pastas removidas Limpeza realizada. %d pastas removidas - Após %1$s minuto Após %1$s minutos Após %1$s minutos - Concluído em %1$s com %2$s erro Concluído em %1$s com %2$s erros Concluído em %1$s com %2$s erros - %1$d página %1$d páginas %1$d páginas - %d atualização pendente %d atualizações pendentes %d atualizações pendentes - Extensão atualizada %d extensões atualizadas %d extensões atualizadas - Pulando %d capítulo, ou ele está faltando na fonte, ou ele foi filtrado Pulando %d capítulos, ou eles estão faltando na fonte, ou eles foram filtrados Pulando %d capítulos, ou eles estão faltando na fonte, ou eles foram filtrados - Próximo capítulo não lido Próximos %d capítulos não lidos Próximos %d capítulos não lidos - %d Tipo de série %d Tipos de séries %d Outras séries - %d Idioma %d Idiomas %d Outros Idiomas - %d Fonte %d Fontes %d Outras Fontes - %d estado %d estados %d outros estados - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml index 178f231dfb..3c903dc12e 100644 --- a/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -1,6 +1,5 @@ - Bem Vindo(a)! Vamos definir algumas coisas primeiro. Você sempre pode fazer alterações nas configurações depois também. Começar @@ -17,11 +16,9 @@ Uso de bateria em plano de fundo Evite interrupções para tarefas longas como atualizações da biblioteca, downloads e restauração de backups. Conceder - Local de armazenamento não definido Local inválido: %s Local inválido - Mangás @@ -366,13 +363,11 @@ Procurar por atualizações Tela segura Segurança - Dados e armazenamento Local de armazenamento Uso de armazenamento Disponível: %1$s / Total: %2$s - Backup Criar backup @@ -428,7 +423,6 @@ Usar as últimas preferências de pré-migração salvas e fontes para migrar em massa Você também pode migrar selecionando o mangá na sua biblioteca - Repositórios de extensões Adicionar novo Repositório @@ -441,7 +435,6 @@ Substituir Assinatura já existente O repositório %1$s tem a mesma assinatura que %2$s.\nSe isso é esperado, %2$s será substituído, caso contrário entre em contato com o dono do repositório. - Versão Data de compilação @@ -797,9 +790,6 @@ Marcar um intervalo de capítulos como lido Cancelar todos para esta série Atalhos do app - TachiyomiJ2K requer acesso total ao armazenamento do Android 11 para baixar capítulos, criar backups automáticos e ler mangás locais. -\n -\nNa próxima tela ative \"Permitir acesso de gerenciamento de todos arquivos.\" Tema escuro Tema claro Não foi encontrado resultado similar @@ -808,8 +798,6 @@ Atualizações globais Abrir uma série aleatória Mostrar o número de itens - TachiyomiJ2K requer o acesso a todos os arquivos para baixar os capítulos. Toque aqui, então habilite \"Permitir o acesso para gerenciar todos os arquivos\" - Permissão para acessar arquivos Backup/restauração pode não funcionar adequadamente se a Otimização MIUI estiver desabilitada. Definir como padrão Adicionar tag @@ -996,7 +984,6 @@ Violeta 5% User Agent padrão - O formato RARv5 não é suportado Download automático durante a leitura Capítulos disponíveis offline Status @@ -1054,7 +1041,7 @@ Aplicar Informações de depuração Atividade em segundo plano - Revogar todas as extensões confiáveis + Revogar todas as extensões confiáveis Incluir configurações sensíveis (por exemplo, tokens de login de rastreadores) Falha ao adquirir acesso persistente à pasta. O aplicativo pode se comportar de forma inesperada. Erro interno: %s @@ -1079,4 +1066,4 @@ Cortar bordas (tira longa) Auto-anexar ID Abrir repositório de origem - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pt/plurals.xml b/i18n/src/commonMain/moko-resources/pt/plurals.xml index 0f14a524e8..a8e0304e51 100644 --- a/i18n/src/commonMain/moko-resources/pt/plurals.xml +++ b/i18n/src/commonMain/moko-resources/pt/plurals.xml @@ -1,89 +1,69 @@ - Atualização de extensão disponível %d atualizações de extensão disponíveis %d atualizações de extensão disponíveis - %d categoria %d categorias %d categorias - - - %1$d página restante - %1$d páginas restantes - %1$d páginas restantes - - Remover %1$d capítulo transferido\? Remover %1$d capítulos transferidos\? Remover %1$d capítulos transferidos\? - Concluído em %1$s com %2$s erro Concluído em %1$s com %2$s erros Concluído em %1$s com %2$s erros - %1$s capítulo %1$s capítulos - e mais %1$d capítulo e mais %1$d capítulos e mais %1$d capítulos - Para %d título Para %d títulos Para %d títulos - Após %1$s minuto Após %1$s minutos Após %1$s minutos - Limpeza realizada. %d pasta removida Limpeza realizada. %d pastas removidas Limpeza realizada. %d pastas removidas - Cache limpa. %d ficheiro foi apagado Cache limpa. %d ficheiros foram apagados Cache limpa. %d ficheiros foram apagados - %d manga migrada %d mangas migradas %d mangas migradas - Copiar %1$d%2$s manga\? Copiar %1$d%2$s mangas\? Copiar %1$d%2$s mangas\? - Migrar %1$d%2$s manga\? Migrar %1$d%2$s mangas\? Migrar %1$d%2$s mangas\? - Um capítulo foi removido da fonte: \n%2$s @@ -95,58 +75,49 @@ \n%2$s \nApagar as suas transferências\? - %1$d página %1$d páginas %1$d páginas - Extensão atualizada %d extensões atualizadas %d extensões atualizadas - %d atualização pendente %d atualizações pendentes %d atualizações pendentes - Ignorando %d capítulo, ou a fonte está em falta ou foi filtrado Ignorando %d capítulos, ou a fonte está em falta ou foi filtrado Capítulos %d ignorados, ou a fonte está em falta ou foi filtrado - 1 tipo de série %d tipos de séries %d tipos de séries - 1 fonte %d fontes %d fontes - 1 estado %d estados %d estados - 1 idioma %d idiomas %d idiomas - Próximo capítulo não lido Próximos %d capítulos não lidos Próximos %d capítulos não lidos - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/pt/strings.xml b/i18n/src/commonMain/moko-resources/pt/strings.xml index 2e5ab32a5c..bf86fef1a1 100644 --- a/i18n/src/commonMain/moko-resources/pt/strings.xml +++ b/i18n/src/commonMain/moko-resources/pt/strings.xml @@ -787,11 +787,6 @@ Por número do capítulo Pela ordem da fonte Não foi marcado - TachiyomiJ2K requer o acesso total aos ficheiros para descarregar os capítulos. Clique aqui, então ative \"Permitir o acesso para gerir todos os ficheiros\" - TachiyomiJ2K requer acesso a todos os ficheiros no Android 11 para transferir capítulos, criar backups automáticos, e ler mangas locais. -\n -\nNo próximo ecrã, ative \"Permitir acesso de gestão de todos os ficheiros.\" - Permissões de ficheiro necessárias Isto forçará a cache transferida a recalcular. Útil se modificou transferências fora desta app e quer que a app as apanhe Usar navegação lateral Safira sombreado @@ -916,7 +911,6 @@ Melhora o desempenho do leitor Zonas de toque 5% - O formato RARv5 não é suportado Capítulos transferidos Transferência automática durante leitura Apenas funciona em entradas na biblioteca e se o capítulo atual, mais o próximo, já estiverem transferidos @@ -988,4 +982,4 @@ Permitir as notificações é recomendado para manter o aplicativo e sua biblioteca atualizados. Informações de depuração Atividade em segundo plano - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ro/plurals.xml b/i18n/src/commonMain/moko-resources/ro/plurals.xml index 5811d9d04f..4a39b90b64 100644 --- a/i18n/src/commonMain/moko-resources/ro/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ro/plurals.xml @@ -1,66 +1,50 @@ - Actualizare de extensie disponibilă %d actualizări de extensie disponibile %d actualizări de extensie disponibile - Gata în %1$s cu eroarea %2$s Gata în %1$s cu %2$s erori Gata în %1$s cu %2$s erori - %d categorie %d categorii %d categorii - - - %1$d pagină rămasă - %1$d pagini rămase - %1$d pagini rămase - - %1$s capitol %1$s capitole %1$s capitole - Eliminați %1$d capitolul descărcat\? Eliminați %1$d capitole descărcate\? Eliminați %1$d capitole descărcate\? - și %1$d capitol mai mult și %1$d mai multe capitole și %1$d mai multe capitole - Pentru %d titlu Pentru %d titluri Pentru %d titluri - Copiați %1$d%2$s manga\? Copiați %1$d%2$s manga\? Copiați %1$d%2$s manga\? - Migrează %1$d%2$s manga\? Migrează %1$d%2$s manga\? Migrează %1$d%2$s manga\? - Un capitol a fost eliminat din sursă: \n%2$s @@ -74,82 +58,69 @@ \n \nȘtergeți descărcarea acestora\? - %1$d pagină %1$d pagini %1$d pagini - Curățenie făcută. A fost eliminat %d dosar Curățenie făcută. Au fost eliminate %d dosare Curățenie făcută. Au fost eliminate %d dosare - %d manga a migrat %d mangauri au migrat %d mangauri au migrat - După %1$s minut După %1$s minute După %1$s minute - Cache curățat. %d fișierul a fost șters Cache curățat. %d fișiere au fost șterse Cache curățat. %d fișiere au fost șterse - Următorul capitol necitit Următoarele %d capitole necitite Următoarele %d capitole necitite - %d actualizare în așteptare %d actualizări în așteptare %d actualizări în așteptare - Extensia a fost actualizată Extensiile %d au fost actualizate Extensiile %d au fost actualizate - Omiterea %d capitol, fie că sursa lipsește, fie că a fost filtrată Omiterea a %d capitole, fie că sursa nu le are, fie că au fost filtrate Omiterea a %d capitole, fie că sursa nu le are, fie că au fost filtrate - %d Tip de serii %d Tipuri de serii %d Tipuri de serii - %d sursă %d surse %d surse - %d Limbă %d Limbi %d Limbi - %d status %d statusuri %d statusuri - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ro/strings.xml b/i18n/src/commonMain/moko-resources/ro/strings.xml index 1a5bf48696..5e25f5fa05 100644 --- a/i18n/src/commonMain/moko-resources/ro/strings.xml +++ b/i18n/src/commonMain/moko-resources/ro/strings.xml @@ -637,11 +637,6 @@ Aspect Sincronizare unidirecțională pentru actualizarea progresului capitolului în cadrul serviciilor de urmărire. Configurați urmărirea pentru intrările manga individuale din butonul de urmărire al acestora. Niciun capitol de șters - TachiyomiJ2K are nevoie de acces la toate fișierele pentru a descărca capitole. Atingeți aici, apoi activați \"Permiteți accesul pentru a gestiona toate fișierele\". - Permisiuni necesare pentru a accesa fișierele - TachiyomiJ2K necesită acces la toate fișierele din Android 11 pentru a descărca capitole, pentru a crea copii de rezervă automate și pentru a citi manga locală. -\n -\nÎn ecranul următor, activați \"Allow access to manage all files\" (Permiteți accesul pentru a gestiona toate fișierele). Înlăturați toate descărcările\? Editare finalizată Anulat @@ -902,7 +897,6 @@ Selectați sursele dezinstalate Link-uri utile de traducere Nu s-a putut diviza imaginea descărcată - Formatul RARv5 nu este acceptat 5% Unele limbi pot necesita o relansare a aplicației pentru a fi afișate corect Păstrează manga cu capitole citite @@ -953,4 +947,4 @@ Program instalare Intrări în bibliotecă Activități în fundal - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ru/plurals.xml b/i18n/src/commonMain/moko-resources/ru/plurals.xml index 175ad0788a..5b5e27100e 100644 --- a/i18n/src/commonMain/moko-resources/ru/plurals.xml +++ b/i18n/src/commonMain/moko-resources/ru/plurals.xml @@ -1,90 +1,71 @@ - Кэш очищен. %d файл был удален Кэш очищен. %d файла было удалено Кэш очищен. %d файлов было удалено Кэш очищен. %d файлов было удалено - Очистка завершена. %d папка удалена Очистка завершена. %d папки удалено Очистка завершена. %d папок удалено Очистка завершена. %d папок удалено - Копировать %1$d%2$s серию\? Копировать %1$d%2$s серии\? Копировать %1$d%2$s серий\? Копировать %1$d%2$s серий\? - Мигрировать %1$d%2$s серию\? Мигрировать %1$d%2$s серии\? Мигрировать %1$d%2$s серий\? Мигрировать %1$d%2$s серий\? - Через %1$s минуту Через %1$s минуты Через %1$s минут Через %1$s минут - %d серия перемещёна %d серии перемещено %d серий перемещено %d серий перемещено - и ещё %1$d глава и ещё %1$d главы и ещё %1$d глав и ещё %1$d глав - Для %d серии Для %d серий Для %d серий Для %d серий - Удалить %1$d загруженную главу\? Удалить %1$d загруженных глав\? Удалить %1$d загруженных глав\? Удалить %1$d загруженных глав\? - %1$s глава %1$s главы %1$s глав %1$s глав - - - %1$d страница осталась - %1$d страницы осталось - %1$d страниц осталось - %1$d страниц осталось - - %d категория %d категории %d категорий %d категорий - %1$s глава была удалена из источника: \n%2$s @@ -99,81 +80,70 @@ \n%2$s \nУдалить загрузку\? - Доступно %d обновление для расширения Доступны %d обновления для расширений Доступны %d обновлений для расширений Доступны %d обновлений для расширений - Выполнено за %1$s с %2$s ошибкой Выполнено за %1$s с %2$s ошибками Выполнено за %1$s с %2$s ошибками Выполнено за %1$s с %2$s ошибками - %1$d страница %1$d страницы %1$d страниц %1$d страниц - %d обновление ожидается %d обновления ожидаются %d обновлений ожидаются %d обновлений ожидаются - Обновлено %d расширение Обновлено %d расширения Обновлено %d расширений Обновлено %d расширений - Пропущена %d глава, так как она либо отсутствует в источнике, либо была отфильтрована Пропущено %d главы, так как они либо отсутствуют в источнике, либо были отфильтрованы Пропущено %d глав, так как они либо отсутствуют в источнике, либо были отфильтрованы Пропущено %d глав, так как они либо отсутствуют в источнике, либо были отфильтрованы - Следующая непрочитанная глава Следующие %d непрочитанные главы Следующие %d непрочитанных глав Следующие %d непрочитанных глав - %d тип серии %d типа серии %d типов серий %d типов серий - %d серия %d серии %d серий %d серий - %d статус %d статуса %d статусов %d статусов - %d язык %d языка %d языков %d языков - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/ru/strings.xml b/i18n/src/commonMain/moko-resources/ru/strings.xml index 287787a519..4c89bae656 100644 --- a/i18n/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n/src/commonMain/moko-resources/ru/strings.xml @@ -736,9 +736,6 @@ Отметить определённое количество глав, как «Прочитано» Отменить всё для этой серии Ярлыки приложения - TachiyomiJ2K требует доступ ко всем файлам системы Android 11 для загрузки глав, создания автоматических резервных копий и чтения личных серий. -\n -\nНа следующем экране включите «Разрешить доступ к управлению всеми файлами.» Включать: %s Исключать: %s Это заставит пересчитать кэш загрузок. Полезно, если вы изменили загрузки вне этого приложения и хотите, чтобы приложение их подхватило @@ -762,8 +759,6 @@ Глобальные обновления Открыть случайную серию Показать количество серий - TachiyomiJ2K требует доступ ко всем файлам для загрузки глав. Нажмите здесь, затем включите «Разрешить доступ к управлению всеми файлами.» - Требуются права доступа к файлам Ориентация Ориентация по умолчанию Резервная копия/Восстановление может не работать должным образом, если отключена «Оптимизация MIUI». @@ -935,7 +930,6 @@ 5% User agent по умолчанию Некоторым языкам требуется перезапуск приложения для корректного отображения - Формат RARv5 не поддерживается При чтении Загруженные главы Статистика @@ -991,4 +985,98 @@ Настройки приложения Отладочная информация Фоновая активность + Открыть меню расширений/миграции + Открыть глобальный поиск + Реакция на долгий тап в поиске + Старый метод установки ещё не реализован, используем стандартный PackageInstaller + Вредоносные расширения могут получить доступ к чтению и изменению данных входа или выполнять произвольный код.\n\nДоверяя этому расширению, вы действуете на свой страх и риск. + Отозвать доверие у всех расширений + Отозвать доверие у всех расширений? + Длинные страницы + Использовать экспериментальную отрисовку + Случайно + Добавить репозиторий + Давайте установим начальные параметры. Вы всегда можете позже изменить их в настройках. + Стр. %1$d из %2$d + Обновились со старой версии и не уверены, что выбрать? Для более подробного объяснения прочтите пункт \"обновление с Tachiyomi\" в инструкции по настройке хранилища Mihon. + Переустановка при обновлении. + Обрезать поля (Длинные страницы) + Открыть устаревшие настройки области выреза + На устройствах с версией Android старше 9.0, необходимо настраивать область выреза вручную в системных настройках + Выбрать папку + Предоставить + Больше опций + Вверх + Начать + Добро пожаловать! + Выберите папку, где %1$s будет сохранять загруженные главы, бэкапы и прочее.\n\nРекомендуется выбрать отдельную папку.\n\nВыбрана папка: %2$s + Инструкция по настройке хранилища + Обязательно + Не выбрано + Выбрано + Обновления + Разрешить фоновую установку расширений и автоматическое обновление приложения без запроса для устройств с версией Android 11 и старше + Необязательно, но рекомендуется + Предоставьте права приложению + Показывать уведомления + Уведомления об обновлении библиотеки и прочие. + Использование батареи при работе в фоне + Не прерывайте долгие обновления библиотеки, загрузку и восстановление из бэкапа. + Не выбрана папка для хранения + Неверный путь: %s + Неверный путь + Открыть случайную серию (Глобально) + Включить действие при свайпе главы + Открыть последнюю прочитанную главу + Реакция на долгий тап в недавних + Показать очередь загрузки + Показывать содержимое в области выреза + Сохранять страницы в отдельных папках + Создаёт папки по названию манги + Свой профиль отображения + Искать записи на внешних носителях + Исправляет ошибку конфликта загружаемых глав, когда они имеют одно название + Писать логи в системный лог (может снизить производительность) + Включить персональные данные (в т.ч. токены входа в аккаунты трекеров) + Неверная ссылка репозитория + У репозиториев %1$s и %2$s одинаковый отпечаток ключа подписи.\nЕсли так и должно быть, то репозиторий %2$s будет заменён, иначе свяжитесь с держателем репозитория. + Не удалось получить постоянный доступ к папке. Поведение приложения может быть непредсказуемым. + Только что + %s завершилась из-за ошибки. Пожалуйста, сделайте скриншот этого сообщения, сохраните логи вылета, и откройте с ними новую Issue на GitHub. + Свой порог чувствительности маски + Увеличивает чувствительность, если отрисована пустая страница.\nВыбрано: %s + Присоединять ID к имени + Поделиться обложкой + Песок + Вебкомикс + Внутренняя ошибка: %s + Тип содержимого + Папка хранилища + Занятое место + Данные и хранилище + Свободно: %1$s / Всего: %2$s + Добавить новый репозиторий + Репозитории расширений + Заменить + Отпечаток ключа подписи уже существует + Тапните здесь для помощи с Cloudflare + Перезапустить приложение + Обновить + Возникла неожиданная ошибка + Показывать логи + Невозможно перейти по ссылке + Открыть репозиторий + Двойной тап для приближения + Последний автоматический бэкап: %s + Режим отладки + Репозиторий уже добавлен! + Вы ещё не добавили ни одного репозитория. + Вы уверены, что хотите удалить репозиторий \"%s\"? + Переместить серию в конец + Для этого приложения требуется WebView + Назад + Вперёд + По умолчанию (%d) + Удалить репозиторий? + Обновить diff --git a/i18n/src/commonMain/moko-resources/sc/plurals.xml b/i18n/src/commonMain/moko-resources/sc/plurals.xml index e120eef11c..56eaabefb0 100644 --- a/i18n/src/commonMain/moko-resources/sc/plurals.xml +++ b/i18n/src/commonMain/moko-resources/sc/plurals.xml @@ -1,41 +1,33 @@ - B\'at un\'agiornamentu a disponimentu pro un\'estensione B\'ant agiornamentos a disponimentu pro %d estensiones - A pustis de %1$s minutu A pustis de %1$s minutos - Innetadura acabada. %d cartella iscantzellada Innetadura acabada. %d cartellas iscantzelladas - Memòria temporànea isboidada. %d documentu est istadu iscantzelladu Memòria temporànea isboidada. %d documentos sunt istados iscantzellados - %d manga tramudadu %d manga tramudados - Copiare %1$d%2$s manga\? Copiare %1$d%2$s manga\? - Tramudare %1$d%2$s manga\? Tramudare %1$d%2$s manga\? - Unu capìtulu est istadu bogadu dae sa mitza: \n%2$s @@ -45,84 +37,64 @@ \n \nCheres iscantzellare sos iscarrigamentos issoro\? - e %1$d àteru capìtulu e àteros %1$d capìtulos - Pro %d tìtulu Pro %d tìtulos - %d categoria %d categorias - - - Galu %1$d pàgina - Galu %1$d pàginas - - Iscantzellare %1$d capìtulu iscarrigadu\? Iscantzellare %1$d capìtulos iscarrigados\? - Fatu in %1$s cun %2$s errore Fatu in %1$s cun %2$s errores - %1$s capìtulu %1$s capìtulos - %1$d pàgina %1$d pàginas - %d agiornamentu in isetu %d agiornamentos in isetu - Estensione agiornada %d estensiones agiornadas - Brinchende %d capìtulu, sa fonte non lu tenet o est istadu bogadu cun unu filtru Brinchende %d capìtulos, sa fonte non los tenet o sunt istados bogados cun unu filtru - Su capìtulu non lèghidu imbeniente Sos %d capìtulos non lèghidos imbenientes - %d fonte %d fontes - %d istadu %d istados - %d limba %d limbas - %d casta de sèrie %d castas de sèrie - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sc/strings.xml b/i18n/src/commonMain/moko-resources/sc/strings.xml index c4ec13b5c3..00541b2482 100644 --- a/i18n/src/commonMain/moko-resources/sc/strings.xml +++ b/i18n/src/commonMain/moko-resources/sc/strings.xml @@ -729,11 +729,6 @@ Agiornamentos globales Aberi una sèrie a casu Ammustra su nùmeru de elementos - Pro iscarrigare capìtulos TachiyomiJ2K tenet bisòngiu de s\'atzessu a totu sos documentos. Incarca inoghe, e a pustis abìlita \"Permite s\'atzessu pro amministrare totu sos documentos.\" - Pro iscarrigare capìtulos, creare còpias de seguresa e lèghere manga in locale TachiyomiJ2K tenet bisòngiu de s\'atzessu a sos documentos de Android 11. -\n -\nIn s\'ischermada imbeniente abìlita \"Permite s\'atzessu pro amministrare totu sos documentos.\" - Permissos de iscritura pedidos Orientamentu Orientamentu predefinidu Sa còpia de seguresa e su riprìstinu diant pòdere non funtzionare comente si tocat si s\'otimizatzione MIUI est disabilitada. @@ -905,7 +900,6 @@ 5% Istringa de agente de utente predefinida Unas cantas limbas diant pòdere bisongiare chi torres a allùghere s\'aplicatzione pro las ammustrare comente si tocat - Su formadu RARv5 no est suportadu Mantene sos manga cun capìtulos lèghidos Totu sos manga lèghidos Iscàrriga in automàticu durante sa letura @@ -959,4 +953,4 @@ Elementos de sa biblioteca Informatziones de depuratzione de còdighe Atividade in s\'isfundu - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/si/strings.xml b/i18n/src/commonMain/moko-resources/si/strings.xml index 983a9539a9..8e95ce06da 100644 --- a/i18n/src/commonMain/moko-resources/si/strings.xml +++ b/i18n/src/commonMain/moko-resources/si/strings.xml @@ -4,7 +4,6 @@ පරිච්ඡේදය %1$s පරිච්ඡේද %2$d හි %1$d වන පරිච්ඡේදය නම - ෆයිල් සඳහා අවසරය අවශ්‍යයි මන්හ්වා (කොරියන්) මන්හුවා (චීන) කොමික් @@ -15,11 +14,7 @@ නවතා ඇත ප්‍රවේශ වීමට අගුලු හරින්න අන්ලොක් - තචියොමිJ2K සියලුම ෆයිල් සඳහා ප්‍රවේශ වීමට ඉඩ ඉල්ලයි. මෙය ස්වයංක්‍රීය බැකප් සෑදීමටත්, පරිච්ඡේද බාගැනීම් සඳහාත්, දුරකථනයේ ඇති මංගා කියවීම සඳහාත් වේ. -\n -\nඊළග තීරයේ දැක්වෙන \"allow access to manage all files\" සක්‍රීය කරන්න මංගා - තචියොමිJ2K සියලුම ෆයිල් සඳහා ප්‍රවේශ වීමට ඉඩ ඉල්ලයි. මෙය පරිච්ඡේද බාගැනීම් සඳහා වේ. මෙතන ඔබා, \"Allow access to manage all files.\" සක්‍රීය කරන්න කියවීමට පටන් අරන් නෑ චිත්‍ර ශිල්පියා කියවමින් පවතී diff --git a/i18n/src/commonMain/moko-resources/sk/plurals.xml b/i18n/src/commonMain/moko-resources/sk/plurals.xml index b739e82c7d..2397597471 100644 --- a/i18n/src/commonMain/moko-resources/sk/plurals.xml +++ b/i18n/src/commonMain/moko-resources/sk/plurals.xml @@ -1,63 +1,48 @@ - Je dostupná aktualizácia pre rozšírenie Je dostupných %d aktualizácii pre rozšírenia Sú dostupné %d aktualizácii pre rozšírenia - %d kategória %d kategórie %d kategórií - Dokončené za %1$s s %2$s chybou Dokončené za %1$s s %2$s chybami Dokončené za %1$s s %2$s chybami - Ďalšia neprečítaná kapitola Ďalšie %d neprečítané kapitoly Ďalších %d neprečítaných kapitol - %1$s kapitola %1$s kapitoly %1$s kapitol - Odstrániť %1$d stiahnutú kapitolu\? Odstrániť %1$d stiahnute kapitoly\? Odstrániť %1$d stiahnutých kapitol\? - - - Zostáva %1$d strana - Zostávú %1$d strany - Zostáva %1$d stran - - Po %1$s minúte Po %1$s minútach Po %1$s minútach - Pre %d titul Pre %d tituly Pre %d titulov - Preskočenie %d kapitoly, buď zdroj chýba, alebo bol odfiltrovaný Preskočenie %d kapitol, buď zdroj chýba, alebo bol odfiltrovaný Zložky %d kapitoly, buď zdroj chýba, alebo boli filtrované - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sk/strings.xml b/i18n/src/commonMain/moko-resources/sk/strings.xml index cd7e4bcb7b..3186c5b81d 100644 --- a/i18n/src/commonMain/moko-resources/sk/strings.xml +++ b/i18n/src/commonMain/moko-resources/sk/strings.xml @@ -288,7 +288,6 @@ Zrušiť všetko pre túto sériu Zabezpečená obrazovka Obrázok sa nepodarilo načítať - Požadované povolenia súborov Podľa dátumu nahratia Upozornenie Sledované @@ -315,10 +314,6 @@ Vždy Manhwa Komix - TachiyomiJ2K vyžaduje prístup ku všetkým súborom v Android 11 na sťahovanie mangy, vytváranie automatických záloh a čítania stiahnutej mangy. -\n -\nNa nasledujucej obrazovke, povoľte \"Povoliť prístup ku spravovaniu všetkych súborov.\" - TachiyomiJ2K vyžaduje prístup ku všetkým súborom pre stiahnutie časti. Kliknite sem, a potom potvrďte \"Povoliť prístup na spravovanie všetkých súborov\" Vzostupne Zostupne Vyberte inverzne @@ -368,7 +363,6 @@ Optimalizácia batérie je už vypnutá 10% V tvare písmena L - Formát RARv5 nie je podporovaný Žiadne kapitoly na odstránenie Zobrazenie prázdnych kategórií pri filtrovaní Žiadne zhody pre vaše filtre @@ -570,4 +564,4 @@ Inštalátor Záznamy v knižnici Aktivita na pozadí - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sq/strings.xml b/i18n/src/commonMain/moko-resources/sq/strings.xml index ebe0c9c8e3..2afffa51e4 100644 --- a/i18n/src/commonMain/moko-resources/sq/strings.xml +++ b/i18n/src/commonMain/moko-resources/sq/strings.xml @@ -266,7 +266,6 @@ Ruaje si arkiv CBZ Kapitulli i fundit Majtas - Formati RARv5 nuk mbështetet Migroni Më tepër Varg i pavlefshëm i agjentit të përdoruesit @@ -411,4 +410,4 @@ %1$d hyrje jashtë bibliotekës në bazën e të dhënave Fshi historikun për shënimet që nuk janë ruajtur në bibliotekën tënde Aktiviteti në sfond - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sr/plurals.xml b/i18n/src/commonMain/moko-resources/sr/plurals.xml index 3d081648d7..14a8db3c81 100644 --- a/i18n/src/commonMain/moko-resources/sr/plurals.xml +++ b/i18n/src/commonMain/moko-resources/sr/plurals.xml @@ -1,72 +1,55 @@ - Доступна је нова верзија екстензије %d нове верзије екстензије су доступне %d нових верзија екстензија су доступна - %d kategorija %d kategorije %d kategoriji - Прескаче се %d поглавље, или не постоји у извору или је филтером издвојено Прескаче се %d поглавља, или не постоји у извору или је филтером издвојено Прескаче се %d поглавља, или не постоји у извору или је филтером издвојено - Завршено у %1$s са %2$s грешком Завршено у %1$s са %2$s грешке Завршено у %1$s са %2$s грешака - Keš je obrisan. %d datoteka je izbrisana Keš je obrisan. %d datoteke su izbrisane Keš je obrisan. %d datoteke su izbrisane - Kopiraj %1$d%2$s mangu\? Kopiraj %1$d%2$s mange\? Koporaj %1$d%2$s mangi\? - Nakon %1$s minut Nakon %1$s minuta Nakon %1$s minuta - %d ažuriranje je na čekanju %d ažuriranja su na čekanju %d ažuriranje je na čekanju - %1$d strana %1$d strane %1$d Stranice - Čišćenje završeno.Uklonjena %d fascikla Čišćenje završeno.Uklonjene %d fascikle Čišćenje završeno.Uklonjenj %d fascikli - - - %1$d strana ostala - %1$d strana ostalo - %1$d strana ostalo - - Поглавље је уклоњено из извора: \n%2$s @@ -80,76 +63,64 @@ \n \nИзбрисати преузимања\? - %d manga prebačena %d mangi prebačeno %d mangi prebačeno - Premestite %1$d%2$s mangu\? Premestine %1$d%2$s mange\? Premestite %1$d%2$s mangi\? - %1$s poglavlje %1$s poglavlja %1$s poglavlja - Za %d naslov Za %d naslove Za %d naslovi - Još %1$d naslov Još %1$d naslova Jos %1$d naslova - Izbrisati %1$d preuzeto poglavlje\? Izbrisati %1$d preuzeta poglavlja\? Izbrisati %1$d preuzetih poglavlja\? - Екстензија ажурирана %d екстензије су ажуриране %d екстензија је ажурирано - Следеће непрочитано поглавље Следећа %d непрочитана поглавља Следећих %d непрочитаних поглавља - %d врста серије %d врсте серије %d врста серија - %d статус %d статуса %d статуса - %d извор %d извора %d извора - %d језик %d језика %d језика - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sr/strings.xml b/i18n/src/commonMain/moko-resources/sr/strings.xml index f3dfe0d43d..b3baa9fb55 100644 --- a/i18n/src/commonMain/moko-resources/sr/strings.xml +++ b/i18n/src/commonMain/moko-resources/sr/strings.xml @@ -601,7 +601,6 @@ Omogućite samo zakačene izvore za migraciju Dodatak za obaveštenje je ažuriran Upravljate onime što se preuzima - Potrebna je dozvola za datoteku Strip Nepočeto U toku @@ -671,10 +670,6 @@ Oznaceno kao pročitano Nova poglavlja Kategorija - TachiyomiJ2K je potreban pristup svim datotekama u Andorid 11 za preuzimanje poglavlja, pravljenje automatskih rezervin kopija, i čitanje lokalnih mangi. -\n -\nNa sledećem ekranu, dozvolu \"Dozvoli pristum za upravljanje svih datoteka.\" - TachiyomiJ2K traži pristum svim datotekama povodom preuzimanja ppglavlja.Pritisni ovde,a onda dozvoli \"Dozvolite pristum za upravljanje svim datotekama.\" Po redosledu izvora Kategorija sa tim imenom već postoji! Nije moguće instalirati ažuriranje @@ -903,7 +898,6 @@ Прескочи дупликатска поглавља Дозволи брисање забележених поглавља Трекери - RARv5 формат није подржан Није могуће разделити преузету слику Завршена листа Наслови на чекању @@ -958,4 +952,4 @@ Наслови колекције Информације за отклањање грешака Активност у позадини - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sv/plurals.xml b/i18n/src/commonMain/moko-resources/sv/plurals.xml index 8828125957..c8614a951f 100644 --- a/i18n/src/commonMain/moko-resources/sv/plurals.xml +++ b/i18n/src/commonMain/moko-resources/sv/plurals.xml @@ -1,46 +1,33 @@ - Tilläggsuppdatering tillgänglig %d tilläggsuppdateringar tillgängliga - Klar på %1$s med %2$s fel Klar på %1$s med %2$s fel - %d kategori %d kategorier - - - %1$d sida kvar - %1$d sidor kvar - - %1$s kapitel %1$s kapitel - Ta bort %1$d nerladdade kapitel\? Ta bort%1$d nerladdade kapitel\? - och %1$d ytterligare kapitel och %1$d ytterligare kapitel - För %d titel För %d titlar - Ett kapitel har tagits bort från källan: \n%2$s @@ -50,79 +37,64 @@ \n \nTa bort nedladdningen\? - %1$d sida %1$d sidor - Rensning klar. %d mapp har tagits bort Rensning klar. %d mappar har tagits bort - Cache rensad. %d fil har tagits bort Cache rensad. %d filer har tagits bort - %d serie migrerad %d serier migrerade - Kopiera %1$d%2$s serie\? Kopiera %1$d%2$s serier\? - Migrera %1$d%2$s serie\? Migrera %1$d%2$s serier\? - Efter %1$s minut Efter %1$s minuter - %d uppdatering väntar %d uppdateringar väntar - Tillägget uppdaterat %d tillägg uppdaterade - Hoppar över %d kapitel, antingen saknar källan det eller så har det filtrerats bort Hoppar över %d kapitel, antingen saknar källan dem eller så har de filtrerats bort - Nästa olästa kapitel Nästa %d olästa kapitel - %d typ av serie %d typer av serier - %d källa %d källor - %d status %d statusar - %d språk %d språk - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/sv/strings.xml b/i18n/src/commonMain/moko-resources/sv/strings.xml index fd76e23b28..96ff1c5e1e 100644 --- a/i18n/src/commonMain/moko-resources/sv/strings.xml +++ b/i18n/src/commonMain/moko-resources/sv/strings.xml @@ -729,11 +729,6 @@ Globala uppdateringar Öppna en slumpmässig serie Visa antal artiklar - TachiyomiJ2K kräver åtkomst till alla filer för att ladda ner kapitel. Tryck här och aktivera sedan \"Tillåt åtkomst för att hantera alla filer.\" - TachiyomiJ2K kräver tillgång till alla filer i Android 11 för att kunna ladda ner kapitel, skapa automatiska säkerhetskopior, och läsa lokala serier. -\n -\nPå nästa skärm aktiverar du \"Tillåt åtkomst för att hantera alla filer\". - Filbehörigheter krävs Orientering Standardorientering Säkerhetskopiering/återställning kanske inte fungerar korrekt om MIUI-optimering är inaktiverat. @@ -904,7 +899,6 @@ Violett 5% Standardsträng för användaragent - RARv5 formatet stöds inte Nedladdade kapitel Automatisk nedladdning under läsning Statistik @@ -962,4 +956,4 @@ Appinställningar Felsökningsinformation Bakgrundsaktivitet - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/th/plurals.xml b/i18n/src/commonMain/moko-resources/th/plurals.xml index a741c0bd1e..263c7b65a9 100644 --- a/i18n/src/commonMain/moko-resources/th/plurals.xml +++ b/i18n/src/commonMain/moko-resources/th/plurals.xml @@ -1,102 +1,75 @@ - ใช้เวลาไป %1$s โดยมีข้อผิดพลาด %2$s รายการ - %d หมวดหมู่ - มีการอัปเดตส่วนขยาย %d รายการพร้อมใช้งาน - ข้ามตอนที่ %d อาจเป็นเพราะหายมาจากแหล่งที่มาหรือถูกกรองออก - โยกย้าย %1$d%2$s เรื่อง\? - คัดลอก %1$d%2$s เรื่อง\? - ล้างเสร็จสิ้น ลบออกไป %d โฟล์เดอร์ - ต้องการนำ %1$d ตอนที่ดาวน์โหลดไว้ออกหรือไม่\? - %1$s ตอน - %d เรื่องได้โยกย้ายแล้ว - ล้างแคชแล้ว แฟ้ม %d รายการได้ถูกลบ - หลังจาก %1$s นาที - ของเรื่อง %d - และอีก %1$d ตอน - อัปเดต %d รายการที่รอดำเนินการ - อัปเดตแล้ว %d ส่วนขยาย - %1$d หน้า - %1$s ตอนได้ถูกนำออกจากแหล่งที่มาแล้ว: \n%2$s \n \nต้องการลบการดาวน์โหลด\? - - - เหลือ %1$d หน้า - - %d ตอนที่ยังไม่ได้อ่านถัดไป - %d ประเภทเรื่อง - %d สถานะ - %d ภาษา - %d แหล่งที่มา - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/th/strings.xml b/i18n/src/commonMain/moko-resources/th/strings.xml index a9496a499b..32585d1d01 100644 --- a/i18n/src/commonMain/moko-resources/th/strings.xml +++ b/i18n/src/commonMain/moko-resources/th/strings.xml @@ -411,9 +411,6 @@ คู่มือเริ่มต้นใช้งาน Shizuku ไม่ทำงาน กำลังอ่าน - TachiyomiJ2K ต้องการการเข้าถึงไฟล์ทั้งหมดใน Android 11 เพื่อดาวน์โหลดตอน สร้างการสำรองข้อมูลอัตโนมัติ และอ่านซีรีย์ในเครื่อง -\n -\nในหน้าจอถัดไป ให้เปิดใช้งาน \"อนุญาตการเข้าถึงเพื่อจัดการไฟล์ทั้งหมด\" ปลดล็อก หมวดหมู่ แสดงหมวดหมู่ว่างเปล่าเมื่อกรอง @@ -679,8 +676,6 @@ ลบตอนที่นำออกแล้ว ลบตอนที่ดาวน์โหลดไว้หากแหล่งที่มาได้ลบตอนออกในออนไลน์ การลบอัตโนมัติ - จำเป็นต้องมีสิทธิ์ในการเข้าถึงไฟล์ - TachiyomiJ2K ต้องการการเข้าถึงไฟล์ทั้งหมดเพื่อดาวน์โหลดตอน แตะที่นี่ แล้วเปิดใช้งาน \"อนุญาตการเข้าถึงเพื่อจัดการไฟล์ทั้งหมด\" ยังไม่เริ่ม ไม่ระบุสถานะ ปลดล็อกเพื่อเข้าถึงคลัง @@ -905,7 +900,6 @@ 5% ตัวแทนผู้ใช้เริ่มต้น บางภาษาอาจจำเป็นต้องปิดเปิดแอปใหม่ เพื่อให้แสดงได้อย่างถูกต้อง - ไม่รับรองรูปแบบ RARv5 รายการที่อ่านหมดแล้ว ละเว้นรายการที่มีตอนที่อ่านไว้ ดาวน์โหลดอัตโนมัติขณะกำลังอ่าน @@ -962,4 +956,4 @@ การตั้งค่าแอป ข้อมูลดีบัก กิจกรรมเบื้องหลัง - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/tl/strings.xml b/i18n/src/commonMain/moko-resources/tl/strings.xml index 69eab30f00..0c8e2fa5c1 100644 --- a/i18n/src/commonMain/moko-resources/tl/strings.xml +++ b/i18n/src/commonMain/moko-resources/tl/strings.xml @@ -303,6 +303,5 @@ Linisin ang mga nadownload na kabanata Pangkalahatang Paghahanap Pangalan - Kailangan ng pahintulot sa file Tako - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/tr/plurals.xml b/i18n/src/commonMain/moko-resources/tr/plurals.xml index 34e1a5eb9a..cb82341e6a 100644 --- a/i18n/src/commonMain/moko-resources/tr/plurals.xml +++ b/i18n/src/commonMain/moko-resources/tr/plurals.xml @@ -1,76 +1,57 @@ - Uzantı güncellemesi var %d uzantı güncellemesi var - %d kategori %d kategoriler - - - %1$d sayfa kaldı - %1$d sayfa kaldı - - İndirilmiş %1$d bölüm silinsin mi\? İndirilmiş %1$d bölüm silinsin mi\? - %1$s içinde %2$s hatayla tamamlandı %1$s içinde %2$s hatayla tamamlandı - %1$d sayfa %1$d sayfa - ve %1$d bölüm daha ve %1$d bölüm daha - %d başlık için %d başlık için - %1$s bölüm %1$s bölüm - Önbellek temizlendi. %d dosya silindi Önbellek temizlendi. %d dosya silindi - %1$d%2$s manga kopyala\? %1$d%2$s manga kopyala\? - %d manga geçiş yaptı %d manga geçiş yaptı - %1$d%2$s mangayı geçiş yap\? %1$d%2$s mangayı geçiş yap\? - Temizleme bitti. %d klasörü kaldırıldı Temizleme bitti. %d klasörü kaldırıldı - Kaynaktan bir bölüm kaldırıldı: \n%2$s @@ -80,49 +61,40 @@ \n \nİndirmesi silinsin mi\? - %1$s dakika sonra %1$s dakika sonra - 1 güncelleme beklemede %d güncelleme beklemede - Uzantı güncellendi %d uzantı güncellendi - %d bölüm atlanıyor, ya kaynakta yok ya da süzgeçlenmiş %d bölüm atlanıyor, ya kaynakta yok ya da süzgeçlenmiş - Sonraki okunmayan bölüm Sonraki %d okunmayan bölüm - %d kaynak %d kaynak - %d durum %d durum - %d dil %d dil - %d seri türü %d seri türü - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/tr/strings.xml b/i18n/src/commonMain/moko-resources/tr/strings.xml index 8df178a605..9bc05e96bc 100644 --- a/i18n/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n/src/commonMain/moko-resources/tr/strings.xml @@ -729,11 +729,6 @@ Uzantı yüklenemedi Genel güncellemeler Öge sayısını göster - TachiyomiJ2K\'in, bölümleri indirmek için tüm dosyalara erişmesi gerekir. Buraya dokunun, ardından \"Tüm dosyaları yönetmek için erişime izin ver\"i etkinleştirin - TachiyomiJ2K\'in, bölümleri indirmek, otomatik yedeklemeler oluşturmak ve yerel mangaları okumak için Android 11\'deki tüm dosyalara erişimesi gerekir. -\n -\nBir sonraki ekranda, \"Tüm dosyaları yönetmek için erişime izin ver\" seçeneğini etkinleştirin - Dosya izinleri gerekli Varsayılan yön Yön Yedekleme/geri yükleme, MIUI optimizasyonu devre dışıysa düzgün çalışmayabilir. @@ -905,7 +900,6 @@ %5 Bazı dillerin doğru görüntülenmesi için uygulamanın yeniden başlatılması gerekebilir Öntanımlı kullanıcı aracısı dizgesi - RARv5 biçimi desteklenmiyor Tüm okunan manga Okunan bölümleri olan mangaları tut Durum @@ -962,4 +956,6 @@ Arka plan etkinliği Kaynak ayarları Uygulama ayarları - + Daha fazla + Hoşgeldiniz! + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/uk/plurals.xml b/i18n/src/commonMain/moko-resources/uk/plurals.xml index d72995b822..85042abb7a 100644 --- a/i18n/src/commonMain/moko-resources/uk/plurals.xml +++ b/i18n/src/commonMain/moko-resources/uk/plurals.xml @@ -1,69 +1,53 @@ - Наявне %d оновлення для розширення %d оновлення для розширень доступні %d оновлень для розширень доступні %d оновлень для розширень доступні - - - %1$d сторінка залишилась - %1$d сторінок залишилось - %1$d сторінок залишилось - %1$d сторінок залишилось - - Видалити %1$d завантажений розділ\? Видалити %1$d завантажених розділів\? Видалити %1$d завантажених розділів\? Видалити %1$d завантажених розділів\? - Для %d заголовку Для %d заголовків Для %d заголовків Для %d заголовків - та %1$d ще розділ та %1$d розділів та %1$d розділів та %1$d розділів - Кеш очищено. %d файл було видалено Кеш очищено. %d файлів було видалено Кеш очищено. %d файлів було видалено Кеш очищено. %d файлів було видалено - %d мангу змігровано %d манґи змігровано %d манґи змігровано %d манґи змігровано - Копіювати %1$d%2$s манґу\? Копіювати %1$d%2$s манґи\? Копіювати %1$d%2$s манґи\? Копіювати %1$d%2$s манґи\? - Мігрувати %1$d%2$s манґу\? Мігрувати %1$d%2$s манґ\? Мігрувати %1$d%2$s манґ\? Мігрувати %1$d%2$s манґ\? - Розділ було видалено з джерела: \n%2$s @@ -81,102 +65,88 @@ \n \nВидалити завантажене\? - Очистку завершено. %d тека видалена Очистку завершено. %d тек видалено Очистку завершено. %d тек видалено Очистку завершено. %d тек видалено - Через %1$s хвилину Через %1$s хвилин Через %1$s хвилин Через %1$s хвилин - Зроблено за %1$s з %2$s помилкою Зроблено за %1$s з %2$s помилками Зроблено за %1$s з %2$s помилками Зроблено за %1$s з %2$s помилками - Пропускається %d розділ, тому що джерело не має його, або він був відфільтрований Пропускаються %d розділи, тому що джерело не має їх, або вони були відфільтровані Пропускання %d розділів, тому що джерело не має їх, або вони були відфільтровані Пропускання %d розділів, тому що джерело не має їх, або вони були відфільтровані - %d категорія %d категорії %d категорій %d категорій - %d Розширення оновлено %d розширення оновлені %d розширеннь оновлені %d розширеннь оновлені - %d оновлення в очікуванні %d оновлення в очікуванні %d оновлень в очікуванні %d оновлень в очікуванні - %1$s розділ %1$s розділи %1$s розділів %1$s розділів - %1$d сторінка %1$d сторінки %1$d сторінок %1$d сторінок - Наступний непрочитаний розділ Наступні %d непрочитані розділи Наступні %d непрочитані розділи Наступні %d непрочитані розділи - %d тип серії %d типи серій %d типів серій %d типів серій - %d серія %d серій %d серій %d серій - %d статус %d статусів %d статусів %d статусів - %d мова %d мов %d мов %d мов - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/uk/strings.xml b/i18n/src/commonMain/moko-resources/uk/strings.xml index 77b4bf2117..15d0549f54 100644 --- a/i18n/src/commonMain/moko-resources/uk/strings.xml +++ b/i18n/src/commonMain/moko-resources/uk/strings.xml @@ -624,12 +624,7 @@ Попередження Попередження: великий об\'єм завантажень може призвести до сповільнення роботи джерел та/або блокуванню Tachiyomi. Що 3 дні - TachiyomiJ2K потребує доступу до всіх файлів в Android 11, щоб завантажувати розділи, створювати автоматичні резервні копії та читати локальну манґу. -\n -\nНа наступному екрані ввімкніть \"Дозволити доступ для керування всіма файлами.\" - TachiyomiJ2K потребує доступу до всіх файлів для завантаження розділів. Натисніть тут, а потім увімкніть \"Дозволити доступ для керування всіма файлами.\" Нова категорія - Потрібен дозвіл доступу до файлів Не в закладинках Підказки щодо пошуку періодично з’являтимуться. Довго натисніть на пропозицію, щоб шукати це. Немає нещодавно прочитаної чи оновленої манґи @@ -904,7 +899,6 @@ Фіолетовий 5% Типовий user agent - Формат RARv5 не підтримується Завантажені розділи Автоматичне завантаження під час читання Статистика @@ -959,4 +953,4 @@ Записи бібліотеки Інформація про налагодження Фонова активність - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/vi/plurals.xml b/i18n/src/commonMain/moko-resources/vi/plurals.xml index 45e45c6456..2145ce46a3 100644 --- a/i18n/src/commonMain/moko-resources/vi/plurals.xml +++ b/i18n/src/commonMain/moko-resources/vi/plurals.xml @@ -1,101 +1,74 @@ - Có %d bản cập nhật của các tiện ích bổ sung - %d danh mục - - - còn %1$d trang - - Xóa %1$d chương đã tải\? - Hoàn tất trong %1$s với %2$s lỗi - Sau %1$s phút - Bộ nhớ đệm tạm đã được làm sạch. Tập tin %d đã được xoá - Làm sạch xong. Đã xoá %d thư mục - Đã di chuyển %d truyện - Sao chép %1$d%2$s \? - Di chuyển %1$d%2$s \? - %1$s chương đã bị xoá khỏi nguồn: \n%2$s \nXoá những gì chúng dã tải\? - %1$d trang - và %1$d chương - Cho %d tiêu đề - %1$s chương - Đã bỏ qua %d chương, vì nguồn đang bị thiếu hoặc đã bị lọc ra - %d tiện ích mở rộng đã được cập nhật - %d bản cập nhật đang chờ - %d chương chưa đọc tiếp - %d loại loạt truyện - %d ngôn ngữ - %d trạng thái - %d nguồn - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/vi/strings.xml b/i18n/src/commonMain/moko-resources/vi/strings.xml index 627acd1068..378a4ba37a 100644 --- a/i18n/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n/src/commonMain/moko-resources/vi/strings.xml @@ -747,11 +747,6 @@ Hiển thị danh mục trống khi dùng bộ lọc Theo số thứ tự chương Chưa được đánh dấu - TachiyomiJ2K cần quyền truy cập tất cả các file để tải chương. Ấn vào đây, rồi xác nhận \"Cho phép quản lý tất cả các file.\" - TachiyomiJ2K cần quyền truy cập vào tất cả các tập tin trên Android 11 để tải xuống các chương, tạo bản sao lưu tự động và đọc truyện đã tải xuống. -\n -\nTrong màn hình tiếp theo, hãy cho phép \"Cho phép truy cập để quản lý tất cả tệp.\" - Yêu cầu quyền truy cập tập tin Hỗ trợ dịch thuật Mở một phần truyện ngẫu nhiên Hiển thị số truyện trong danh mục @@ -937,7 +932,6 @@ Sắp xếp bằng thời gian được nhập về 5% Chuỗi đại diện người dùng mặc định - Định dạng RARv5 không được hỗ trợ Một số ngôn ngữ sẽ cần khởi động lại ứng dụng để hiển thị chính xác Tất cả truyện đã đọc Lưu truyện đang đọc @@ -995,4 +989,4 @@ Cài đặt ứng dụng Thông tin Debug Hoạt động ngầm - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml b/i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml index 59646883d2..8b43857a87 100644 --- a/i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml +++ b/i18n/src/commonMain/moko-resources/zh-rCN/plurals.xml @@ -1,101 +1,72 @@ - %d 个扩展插件可更新 - %d 个类别 - 还有 %1$d 章 - 共 %d 个漫画 - - - 剩余 %1$d 页 - - 移除已下载的 %1$d 章? - - 第%1$s章已从图源中删除: -\n%2$s -\n是否删除其下载内容? + %1$s个章节在以下图源中已被删除:\n%2$s\n是否删除相应的下载内容? - %d 漫画已迁移 - 复制 %1$d%2$s 漫画? - 迁移 %1$d%2$s 漫画? - 在%1$s分钟之后 - 清除完成,移除了 %d 个目录 - 缓存已清除,%d 的文件已被删除 - 耗时 %1$s,出现 %2$s 个错误 - %1$s 章 - %1$d页 - %d 个待处理更新 - 更新了 %d 个扩展 - 跳过了 %d 章,可能是图源没有这些章节,或者被筛选规则排除了 - 下 %d 个未读章节 - %d 种连载类型 - %d 个图源 - %d 种状态 - %d 种语言 - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml index f4b56102dd..0ea27e89bf 100644 --- a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -9,8 +9,8 @@ 书架 书架更新 已选择:%1$d - 扩展 - 扩展信息 + 插件 + 插件信息 筛选 已下载 已添加书签 @@ -88,7 +88,7 @@ 信任 不可信 卸载 - 不可信的插件 + 未被信任的插件 版本: %1$s 语言: %1$s 全屏 @@ -274,7 +274,7 @@ 禁用电池优化 电池优化已被关闭 全局搜索 - 自动检查扩展插件更新 + 自动检查插件更新 官网 开源许可证 电子邮件地址 @@ -508,11 +508,11 @@ 启用缩小 正在阅读 %1$s 设置为全局默认 - 扩展更新 + 插件更新 上次使用 隐藏图源 所有图源 - 搜索拓展… + 搜索插件… 在书架中 重置章节? 搜索近期… @@ -534,7 +534,7 @@ 语言 自动备份 无效的备份文件 - 此插件不是 Tachiyomi 官方插件 + 此插件不是 Tachiyomi 官方插件。 18+ 非官方 可能包含 NSFW (18+) 内容 @@ -558,14 +558,12 @@ 备份不包含任何漫画。 缺少图源: 未登录的追踪源: - 来自备份文件的数据将被还原。 -\n -\n你需要安装所有缺少的扩展,之后登录到跟踪服务以使用它们。 + 来自备份文件的数据将被还原。 \n \n你需要安装所有缺少的插件,之后登录到跟踪服务以使用它们。 已取消还原 %02d 分,%02d 秒 图源迁移指南 NSFW (18+) 图源 - 这并不能防止非官方或可能被错误标记的扩展插件在应用程序中显示 NSFW (18+) 内容。 + 这并不能防止非官方或被错误标记的插件在应用程序中显示 NSFW (18+) 内容。 未找到文件选择应用 下一个 打开日志 @@ -724,29 +722,24 @@ 浅色主题 未找到匹配项 图源不受支持 - 无法安装扩展 + 无法安装插件 全局更新 打开一个随机系列 显示项目数 - TachiyomiJ2K 需要访问所有文件才能下载章节。 点按此处,然后启用“允许访问以管理所有文件” - TachiyomiJ2K 在 Android 11 下需要授权访问所有文件才能进行下载章节、创建自动备份和阅读本地漫画的功能。 -\n -\n在接下来的出现的画面中,请启用“允许访问管理所有文件” - 需要文件权限 方向 默认方向 如果 MIUI 优化被关闭,备份/还原可能无法正常工作。 设为默认 - 要安装扩展必须禁用MIUI优化。 + 必须禁用MIUI优化才能安装插件。 按上传日期 按章节数 按图源顺序 未加为书签 首次打开时忽略刘海屏 - 更平静的你 (动态) - 更明亮的你 (动态) + 更暗淡的Material You (动态) + 更明亮的Material You (动态) 警告 不自动更新 任何网络连接 @@ -754,8 +747,8 @@ 自动更新应用 自动更新 添加标签 - 更新扩展中 - 仍然会提示先安装有些扩展。 + 更新插件中 + 某些插件可能仍然需要先安装。 更新全部 已完成更新 无法安装更新 @@ -765,13 +758,13 @@ 最近安装 最近更新 名称 - 一些扩展可能不会自动更新,如果它们是通过外部途径安装的 - 通知扩展已更新 - 自动更新扩展 + 如果插件是通过外部途径安装的,可能不会被自动更新 + 插件更新后发送通知 + 自动更新插件 拆分双页 - 扩展已更新 + 插件已更新 已安装 %1$s - 待更新扩展 + 插件待更新 影响书架网格封面 限制:%1$s 保留 %1$s 上的两个,仅本地替换 @@ -802,7 +795,7 @@ 扫译小组 错误 大型更新可能会导致电池使用量增加和图源变慢。轻按了解更多。 - 显示在图源和扩展列表中 + 显示在图源和插件列表中 显示封面图轮廓 Shizuku 未在运行 安装并启动 Shizuku 以将 Shizuku 用作插件安装程序。 @@ -894,8 +887,8 @@ 分割长图 可以改善阅读器的性能 阅读中 - 计划读 - 已完结 + 想读 + 读过 搁置中 已放弃 按获取时间排序 @@ -905,7 +898,6 @@ 5% 某些语言可能需要重启应用才能正确显示 默认 User Agent 字符串 - 不支持 RARv5 格式 所有已读漫画 保留有已读章节的漫画 统计详情 @@ -956,10 +948,113 @@ 排除的分类 安装程序 书架中的作品 - 可允许插件在不需要用户确认的情况下安装并对 Android 12 以下的设备启用自动更新 + 允许插件在不需要用户确认的情况下安装,并且在 Android 12 以下的设备上启用自动更新 应用 图源设置 应用设置 调试信息 后台活动 + 更多设置 + 返回 + 选择文件夹 + 存储指南 + 如果从旧版更新而来、不清楚如何选择,可以查看存储指南中的从 Tachiyomi 迁移部分获得更多信息。 + 欢迎! + 首先调整一些默认设置。您也可以稍后在设置中更改它们。 + 开始使用 + 选择一个文件夹供 %1$s 存放下载的章节、备份文件等。\n\n推荐使用一个专门的文件夹。\n\n已选择文件夹:%2$s + 必要 + 可选(推荐) + 用于发送书架更新等通知。 + 后台运行权限 + 随机打开作品(全局) + 撤销所有已信任的插件? + 条漫 + 在屏幕刘海区域显示内容 + Doki + 获取永久文件存储权限失败,应用程序可能会出现异常。 + 点击这里查看 Cloudflare 帮助 + 本应用需要 WebView 才能运行 + 后退 + 安装应用权限 + 用于在更新时安装应用。 + 通知权限 + 避免需要长时间运行的书架更新、下载、备份恢复任务被中断。 + 允许 + 未设置存储位置 + 无效位置:%s + 无效位置 + 第%1$d页,共%2$d页 + 启用章节左右滑动操作 + 显示下载队列 + 长按“最近” + 长按“浏览” + 默认 + 旧版安装程序尚未实现,目前回退到 PackageInstaller(默认) + 撤销所有已信任的插件 + 打开旧版屏幕刘海设置 + 在 Android 9.0 之前的设备上,您需要在系统设置中手动调整屏幕刘海设置 + 将图片保存到单独的文件夹 + 根据作品标题创建文件夹 + 自定义校色文件 + 默认 (%d) + 存储占用 + 可用:%1$s/总共:%2$s + 包含敏感设置(例如进度记录平台的账号信息) + 上次自动备份时间:%s + 调试模式 + 扫描外部存储上的漫画 + 插件仓库 + 添加新仓库 + 添加仓库 + 仓库网址无效 + 此仓库已存在! + 您尚未添加任何仓库。 + 删除此仓库? + 确定要删除仓库“%s”吗? + 替换 + 签名密钥指纹已存在 + %1$s 仓库的签名密钥指纹与 %2$s 仓库相同。\n如果继续,%2$s 将被替换,否则请联系仓库的维护者。 + 打开图源仓库 + 无法打开网址 + 自动附加ID + 刷新 + SFW + NSFW + 内容类型 + %s 发生了意外错误。建议您截屏此消息,转储崩溃日志,然后附加到GitHub Issue中。 + 刷新 + 恶意插件可能会读取所有登录的账号或执行任意代码。\n\n信任此插件代表您愿意承担上述风险。 + 分享封面 + 更新 + 打开全局搜索 + 将详细日志打印到系统日志(会降低应用性能) + 打开最后阅读的章节 + 打开 插件/迁移 菜单 + 双击放大 + 未选中 + 已选中 + 旧版安装程序 + Webtoon + 可能会修复下载的章节同名时相互冲突的问题 + 裁剪边缘(条漫) + 自定义硬件位图阈值 + 数据与存储 + 如果阅读器加载空白图像,请逐步降低阈值。\n已选择: %s + 存储位置 + 详细日志记录 + 将作品移到底部 + 前进 + 发生意外错误 + 刚才 + 内部错误:%s + 重启应用 + 乱序 + 使用实验性的书架UI(基于Compose框架) + 是否确定要删除此项已保存的筛选:“%1$s”? + 已保存的筛选 + 保存名称不可用 + 保存当前的筛选设置? + 保存名称 + 删除此项已保存的筛选设置? diff --git a/i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml b/i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml index 0eaa5972aa..35c116d582 100644 --- a/i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml +++ b/i18n/src/commonMain/moko-resources/zh-rTW/plurals.xml @@ -1,101 +1,74 @@ - %d 個擴充套件可更新 - %d 個分類 - 歷時 %1$s,出現 %2$s 個錯誤 - 略過了 %d 章,也許是來源沒有這些章節,或其已被篩選規則排除 - 共 %1$s 章 - - - 餘 %1$d 頁 - - 已清除快取,%d 個檔案已被刪除 - 移除 %1$d 個已下載的章節? - %1$s 分鐘後 - 共 %d 本漫畫 - 還有 %1$d 章 - 第%1$s章已從來源中刪除: \n%2$s \n是否刪除其下載内容? - 接下來未讀的 %d 章 - %d 個來源 - %d 種語言 - 有%d個更新正在等待 - 更新了%d個擴充套件 - 確定要遷移 %1$d%2$s 個漫畫系列嗎? - 已成功遷移 %d 個漫畫系列 - 確定要複製 %1$d%2$s 個漫畫系列嗎? - %d 系列類型 - %d 狀態 - %1$d 頁 - 清理完成。已刪除 %d 個資料夾 - + \ No newline at end of file diff --git a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml index 036ac8364a..bfb0eb968d 100644 --- a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -453,10 +453,6 @@ 以下章節已讀 最後閱讀的章節%1$s 新章節 - 為了讓您順利下載漫畫章節、自動進行資料備份,以及閱讀儲存在本機的漫畫,TachiyomiJ2K 需要您授予其在 Android 11 環境下的「全檔案存取」權限。 -\n -\n接下來,在出現的畫面中,請啟動「允許存取並管理所有檔案」選項。 - Tachiyomi 需要有儲存空間存取權限才能下載漫畫。請允許\"管理儲存空間\" 入門指南 協助翻譯 搜尋擴充套件… @@ -574,7 +570,6 @@ 所有來源 隱藏來源 按住不放也可以重設章節歷史 - 需要檔案權限 永遠顯示目前分類 書櫃分組… 來源未安裝 @@ -674,7 +669,6 @@ 包含在全域性更新中 全域性更新 如果在這裡禁用某些按鈕,可以在其他地方找到 - 不支援 RARv5 格式 預先下載 閱讀時自動下載 已下載章數 @@ -963,4 +957,22 @@ 偵錯資訊 背景活動 分享封面 - + 更多選擇 + 返回 + 歡迎! + 先選擇一些預設值,您可以隨時在稍後的設定中更改這些東西 + 開始使用 + 打開隨機作品 + 顯示下載佇列 + 打開最後閱讀的章節 + 打開全局搜尋 + 邊緣裁剪(長圖) + 無效位置 + 已選中 + 長圖 + 重啟應用程式 + 刷新 + 預設 + 未選中 + 更新 + \ No newline at end of file diff --git a/presentation/core/build.gradle.kts b/presentation/core/build.gradle.kts index 2dd788bcb9..bb2b8d8303 100644 --- a/presentation/core/build.gradle.kts +++ b/presentation/core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { - alias(androidx.plugins.library) - alias(kotlinx.plugins.android) + id("yokai.android.library") + id("yokai.android.library.compose") + kotlin("android") } android { @@ -12,6 +13,22 @@ android { } } +kotlin { + compilerOptions { + freeCompilerArgs.addAll( + "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", + "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", + "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + ) + } +} + dependencies { api(libs.material) + + implementation(compose.bundles.compose) } diff --git a/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt b/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt new file mode 100644 index 0000000000..6715a6e07b --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt @@ -0,0 +1,787 @@ +package yokai.presentation.core + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDecay +import androidx.compose.animation.core.animateTo +import androidx.compose.animation.core.spring +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.TopAppBarState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.isFinite +import androidx.compose.ui.unit.isSpecified +import androidx.compose.ui.util.fastCoerceIn +import androidx.compose.ui.util.fastFirst +import androidx.compose.ui.util.fastMaxOfOrNull +import androidx.compose.ui.util.fastSumBy +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Composable replacement for [eu.kanade.tachiyomi.ui.base.ExpandedAppBarLayout] + * + * Copied from [androidx.compose.material3.LargeTopAppBar], modified to mimic J2K's + * [eu.kanade.tachiyomi.ui.base.ExpandedAppBarLayout] behaviors + */ +@Composable +fun ExpandedAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.largeTopAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TwoRowsTopAppBar( + title = title, + titleTextStyle = MaterialTheme.typography.headlineMedium, + smallTitleTextStyle = MaterialTheme.typography.titleLarge, + titleBottomPadding = LargeTitleBottomPadding, + smallTitle = title, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + collapsedHeight = CollapsedContainerHeight, + expandedHeight = ExpandedContainerHeight, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + subtitle = null, + subtitleTextStyle = TextStyle.Default, + smallSubtitle = null, + smallSubtitleTextStyle = TextStyle.Default, + ) +} + +@Composable +private fun TwoRowsTopAppBar( + modifier: Modifier = Modifier, + title: @Composable () -> Unit, + titleTextStyle: TextStyle, + titleBottomPadding: Dp, + smallTitle: @Composable () -> Unit, + smallTitleTextStyle: TextStyle, + subtitle: (@Composable () -> Unit)?, + subtitleTextStyle: TextStyle, + smallSubtitle: (@Composable () -> Unit)?, + smallSubtitleTextStyle: TextStyle, + navigationIcon: @Composable () -> Unit, + actions: @Composable RowScope.() -> Unit, + collapsedHeight: Dp, + expandedHeight: Dp, + windowInsets: WindowInsets, + colors: TopAppBarColors, + scrollBehavior: TopAppBarScrollBehavior? +) { + require(collapsedHeight.isSpecified && collapsedHeight.isFinite) { + "The collapsedHeight is expected to be specified and finite" + } + require(expandedHeight.isSpecified && expandedHeight.isFinite) { + "The expandedHeight is expected to be specified and finite" + } + require(expandedHeight >= collapsedHeight) { + "The expandedHeight is expected to be greater or equal to the collapsedHeight" + } + val expandedHeightPx: Float + val collapsedHeightPx: Float + val titleBottomPaddingPx: Int + LocalDensity.current.run { + expandedHeightPx = expandedHeight.toPx() + collapsedHeightPx = collapsedHeight.toPx() + titleBottomPaddingPx = titleBottomPadding.roundToPx() + } + + // Sets the app bar's height offset limit to hide just the bottom title area and keep top title + // visible when collapsed. + SideEffect { + if (scrollBehavior?.state?.heightOffsetLimit != -expandedHeightPx) { + scrollBehavior?.state?.heightOffsetLimit = -expandedHeightPx + } + } + + // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the + // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or + // collapse. + // This will potentially animate or interpolate a transition between the container color and the + // container's scrolled color according to the app bar's scroll state. + val colorTransitionFraction = scrollBehavior?.state?.bottomCollapsedFraction(collapsedHeightPx, expandedHeightPx) ?: 0f + val appBarContainerColor = { + lerp( + colors.containerColor, + colors.scrolledContainerColor, + FastOutLinearInEasing.transform(colorTransitionFraction) + ) + } + + // Wrap the given actions in a Row. + val actionsRow = + @Composable { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } + val topTitleAlpha = TitleAlphaEasing.transform(colorTransitionFraction) + val bottomTitleAlpha = 1f - colorTransitionFraction + // Hide the top row title semantics when its alpha value goes below 0.5 threshold. + // Hide the bottom row title semantics when the top title semantics are active. + val hideTopRowSemantics = colorTransitionFraction < 0.5f + val hideBottomRowSemantics = !hideTopRowSemantics + + Box( + modifier = + modifier + .drawBehind { drawRect(color = appBarContainerColor()) } + .semantics { isTraversalGroup = true } + .pointerInput(Unit) {} + ) { + Column { + AppBarLayout( + modifier = + Modifier.windowInsetsPadding(windowInsets) + // clip after padding so we don't show the title over the inset area + .clipToBounds(), + scrolledOffset = { + scrollBehavior?.state?.topHeightOffset( + topHeightPx = collapsedHeightPx, + totalHeightPx = expandedHeightPx, + ) ?: 0f + }, + navigationIconContentColor = colors.navigationIconContentColor, + titleContentColor = colors.titleContentColor, + //subtitleContentColor = colors.subtitleContentColor, + subtitleContentColor = colors.titleContentColor, + actionIconContentColor = colors.actionIconContentColor, + title = smallTitle, + titleTextStyle = smallTitleTextStyle, + titleAlpha = { topTitleAlpha }, + titleVerticalArrangement = Arrangement.Bottom, + titleHorizontalAlignment = Alignment.Start, + titleBottomPadding = 0, + subtitle = smallSubtitle, + subtitleTextStyle = smallSubtitleTextStyle, + hideTitleSemantics = hideTopRowSemantics, + navigationIcon = navigationIcon, + actions = actionsRow, + height = collapsedHeight, + ) + AppBarLayout( + modifier = + Modifier + // only apply the horizontal sides of the window insets padding, since the + // top + // padding will always be applied by the layout above + .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal)) + .clipToBounds(), + scrolledOffset = { + scrollBehavior?.state?.bottomHeightOffset( + topHeightPx = collapsedHeightPx, + totalHeightPx = expandedHeightPx, + ) ?: 0f + }, + navigationIconContentColor = colors.navigationIconContentColor, + titleContentColor = colors.titleContentColor, + //subtitleContentColor = colors.subtitleContentColor, + subtitleContentColor = colors.titleContentColor, + actionIconContentColor = colors.actionIconContentColor, + title = title, + titleTextStyle = titleTextStyle, + titleAlpha = { bottomTitleAlpha }, + titleVerticalArrangement = Arrangement.Bottom, + titleHorizontalAlignment = Alignment.Start, + titleBottomPadding = titleBottomPaddingPx, + hideTitleSemantics = hideBottomRowSemantics, + subtitle = subtitle, + subtitleTextStyle = subtitleTextStyle, + navigationIcon = {}, + actions = {}, + height = expandedHeight - collapsedHeight, + ) + } + } +} + +/** + * Alternative to `() -> Float` but avoids boxing. + */ +internal fun interface FloatProducer { + /** Returns the Float. */ + operator fun invoke(): Float +} + +@Composable +private fun AppBarLayout( + modifier: Modifier, + scrolledOffset: FloatProducer, + navigationIconContentColor: Color, + titleContentColor: Color, + subtitleContentColor: Color, + actionIconContentColor: Color, + title: @Composable () -> Unit, + titleTextStyle: TextStyle, + subtitle: (@Composable () -> Unit)?, + subtitleTextStyle: TextStyle, + titleAlpha: () -> Float, + titleVerticalArrangement: Arrangement.Vertical, + titleHorizontalAlignment: Alignment.Horizontal, + titleBottomPadding: Int, + hideTitleSemantics: Boolean, + navigationIcon: @Composable () -> Unit, + actions: @Composable () -> Unit, + height: Dp, +) { + Layout( + { + Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) { + CompositionLocalProvider( + LocalContentColor provides navigationIconContentColor, + content = navigationIcon + ) + } + if (subtitle != null) { + Column( + modifier = + Modifier.layoutId("title") + .padding(horizontal = TopAppBarHorizontalPadding) + .then( + if (hideTitleSemantics) Modifier.clearAndSetSemantics {} + else Modifier + ) + .graphicsLayer { alpha = titleAlpha() }, + horizontalAlignment = titleHorizontalAlignment + ) { + ProvideContentColorTextStyle( + contentColor = titleContentColor, + textStyle = titleTextStyle, + content = title + ) + ProvideContentColorTextStyle( + contentColor = subtitleContentColor, + textStyle = subtitleTextStyle, + content = subtitle + ) + } + } else { // TODO(b/352770398): Workaround to maintain compatibility + Box( + modifier = + Modifier.layoutId("title") + .padding(horizontal = TopAppBarHorizontalPadding) + .then( + if (hideTitleSemantics) Modifier.clearAndSetSemantics {} + else Modifier + ) + .graphicsLayer { alpha = titleAlpha() } + ) { + ProvideContentColorTextStyle( + contentColor = titleContentColor, + textStyle = titleTextStyle, + content = title + ) + } + } + Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) { + CompositionLocalProvider( + LocalContentColor provides actionIconContentColor, + content = actions + ) + } + }, + modifier = modifier, + measurePolicy = + remember( + scrolledOffset, + titleVerticalArrangement, + titleHorizontalAlignment, + titleBottomPadding, + height + ) { + TopAppBarMeasurePolicy( + scrolledOffset, + titleVerticalArrangement, + titleHorizontalAlignment, + titleBottomPadding, + height + ) + } + ) +} + +private class TopAppBarMeasurePolicy( + val scrolledOffset: FloatProducer, + val titleVerticalArrangement: Arrangement.Vertical, + val titleHorizontalAlignment: Alignment.Horizontal, + val titleBottomPadding: Int, + val height: Dp, +) : MeasurePolicy { + override fun MeasureScope.measure( + measurables: List, + constraints: Constraints + ): MeasureResult { + val navigationIconPlaceable = + measurables + .fastFirst { it.layoutId == "navigationIcon" } + .measure(constraints.copy(minWidth = 0)) + val actionIconsPlaceable = + measurables + .fastFirst { it.layoutId == "actionIcons" } + .measure(constraints.copy(minWidth = 0)) + + val maxTitleWidth = + if (constraints.maxWidth == Constraints.Infinity) { + constraints.maxWidth + } else { + (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width) + .coerceAtLeast(0) + } + val titlePlaceable = + measurables + .fastFirst { it.layoutId == "title" } + .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth)) + + // Locate the title's baseline. + val titleBaseline = + if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) { + titlePlaceable[LastBaseline] + } else { + 0 + } + + // Subtract the scrolledOffset from the maxHeight. The scrolledOffset is expected to be + // equal or smaller than zero. + val scrolledOffsetValue = scrolledOffset() + val heightOffset = if (scrolledOffsetValue.isNaN()) 0 else scrolledOffsetValue.roundToInt() + + val maxLayoutHeight = max(height.roundToPx(), titlePlaceable.height) + val layoutHeight = + if (constraints.maxHeight == Constraints.Infinity) { + maxLayoutHeight + } else { + (maxLayoutHeight + heightOffset).coerceAtLeast(0) + } + + return placeTopAppBar( + constraints, + layoutHeight, + maxLayoutHeight, + navigationIconPlaceable, + titlePlaceable, + actionIconsPlaceable, + titleBaseline + ) + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurables: List, + height: Int + ) = measurables.fastSumBy { it.minIntrinsicWidth(height) } + + override fun IntrinsicMeasureScope.minIntrinsicHeight( + measurables: List, + width: Int + ): Int { + return max( + height.roundToPx(), + measurables.fastMaxOfOrNull { it.minIntrinsicHeight(width) } ?: 0 + ) + } + + override fun IntrinsicMeasureScope.maxIntrinsicWidth( + measurables: List, + height: Int + ) = measurables.fastSumBy { it.maxIntrinsicWidth(height) } + + override fun IntrinsicMeasureScope.maxIntrinsicHeight( + measurables: List, + width: Int + ): Int { + return max( + height.roundToPx(), + measurables.fastMaxOfOrNull { it.maxIntrinsicHeight(width) } ?: 0 + ) + } + + private fun MeasureScope.placeTopAppBar( + constraints: Constraints, + layoutHeight: Int, + maxLayoutHeight: Int, + navigationIconPlaceable: Placeable, + titlePlaceable: Placeable, + actionIconsPlaceable: Placeable, + titleBaseline: Int + ): MeasureResult = + layout(constraints.maxWidth, layoutHeight) { + // Navigation icon + navigationIconPlaceable.placeRelative( + x = 0, + y = + when (titleVerticalArrangement) { + Arrangement.Bottom -> { + val padding = (maxLayoutHeight - navigationIconPlaceable.height) / 2 + val paddingFromBottom = padding - (navigationIconPlaceable.height - titleBaseline) + val heightWithPadding = paddingFromBottom + navigationIconPlaceable.height + val adjustedBottomPadding = + if (heightWithPadding > maxLayoutHeight) { + paddingFromBottom - (heightWithPadding - maxLayoutHeight) + } else { + paddingFromBottom + } + + layoutHeight - navigationIconPlaceable.height - max(0, adjustedBottomPadding) + } + else -> (layoutHeight - navigationIconPlaceable.height) / 2 + }, + ) + + titlePlaceable.let { + val start = max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width) + val end = actionIconsPlaceable.width + + // Align using the maxWidth. We will adjust the position later according to the + // start and end. This is done to ensure that a center alignment is still maintained + // when the start and end have different widths. Note that the title is centered + // relative to the entire app bar width, and not just centered between the + // navigation icon and the actions. + var titleX = + titleHorizontalAlignment.align( + size = titlePlaceable.width, + space = constraints.maxWidth, + // Using Ltr as we call placeRelative later on. + layoutDirection = LayoutDirection.Ltr + ) + // Reposition the title based on the start and the end (i.e. the navigation and + // action widths). + if (titleX < start) { + titleX += (start - titleX) + } else if (titleX + titlePlaceable.width > constraints.maxWidth - end) { + titleX += ((constraints.maxWidth - end) - (titleX + titlePlaceable.width)) + } + + // The titleVerticalArrangement is always one of Center or Bottom. + val titleY = + when (titleVerticalArrangement) { + Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2 + // Apply bottom padding from the title's baseline only when the Arrangement + // is "Bottom". + Arrangement.Bottom -> { + // Calculate the actual padding from the bottom of the title, taking + // into account its baseline. + val paddingFromBottom = if (titleBottomPadding == 0) { + (maxLayoutHeight - titlePlaceable.height) / 2 + } else { + titleBottomPadding + } - (titlePlaceable.height - titleBaseline) + + // Adjust the bottom padding to a smaller number if there is no room + // to fit the title. + val heightWithPadding = paddingFromBottom + titlePlaceable.height + val adjustedBottomPadding = + if (heightWithPadding > maxLayoutHeight) { + paddingFromBottom - (heightWithPadding - maxLayoutHeight) + } else { + paddingFromBottom + } + + layoutHeight - titlePlaceable.height - max(0, adjustedBottomPadding) + } + // Arrangement.Top + else -> 0 + } + + it.placeRelative(titleX, titleY) + } + + // Action icons + actionIconsPlaceable.placeRelative( + x = constraints.maxWidth - actionIconsPlaceable.width, + y = + when (titleVerticalArrangement) { + Arrangement.Bottom -> { + val padding = (maxLayoutHeight - actionIconsPlaceable.height) / 2 + val paddingFromBottom = padding - (actionIconsPlaceable.height - titleBaseline) + val heightWithPadding = paddingFromBottom + actionIconsPlaceable.height + val adjustedBottomPadding = + if (heightWithPadding > maxLayoutHeight) { + paddingFromBottom - + (heightWithPadding - maxLayoutHeight) + } else { + paddingFromBottom + } + + layoutHeight - actionIconsPlaceable.height - max(0, adjustedBottomPadding) + } + else -> (layoutHeight - actionIconsPlaceable.height) / 2 + }, + ) + } +} + +@Composable +internal fun ProvideContentColorTextStyle( + contentColor: Color, + textStyle: TextStyle, + content: @Composable () -> Unit +) { + val mergedStyle = LocalTextStyle.current.merge(textStyle) + CompositionLocalProvider( + LocalContentColor provides contentColor, + LocalTextStyle provides mergedStyle, + content = content + ) +} + +private fun interface ScrolledOffset { + fun offset(): Float +} + +private suspend fun settleAppBar( + state: TopAppBarState, + velocity: Float, + topHeightPx: Float, + totalHeightPx: Float, + flingAnimationSpec: DecayAnimationSpec?, + snapAnimationSpec: AnimationSpec?, +): Velocity { + // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, + // and just return Zero Velocity. + // Note that we don't check for 0f due to float precision with the collapsedFraction + // calculation. + if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.01f || state.topCollapsedFraction(topHeightPx, totalHeightPx) == 1f) { + return Velocity.Zero + } + var remainingVelocity = velocity + // In case there is an initial velocity that was left after a previous user fling, animate to + // continue the motion to expand or collapse the app bar. + if (flingAnimationSpec != null && abs(velocity) > 1f) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity, + ) + .animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + state.heightOffset = initialHeightOffset + delta + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } + } + // Snap if animation specs were provided. + if (snapAnimationSpec != null) { + if (state.topHeightOffset(topHeightPx, totalHeightPx) < 0 && state.topHeightOffset(topHeightPx, totalHeightPx) > -topHeightPx) { + AnimationState(initialValue = state.topHeightOffset(topHeightPx, totalHeightPx)).animateTo( + if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.5f) { + 0f + } else { + -topHeightPx + }, + animationSpec = snapAnimationSpec + ) { + state.heightOffset = value + (topHeightPx - totalHeightPx) + } + } + } + + return Velocity(0f, remainingVelocity) +} + +/** + * Default values: + * - Top app bar height: 128px + * - Total app bar height: 304px + * - Bottom app bar height: 176px + * - Top offset limit: (-(Total), (Top - Total)) = (-304px, -176px) + * - Bottom offset limit: ((Top - Total), 0) = (-176px, 0px) + */ + +private fun TopAppBarState.rawTopHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { + return heightOffset + (totalHeightPx - topHeightPx) +} + +private fun TopAppBarState.topHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { + return rawTopHeightOffset(topHeightPx, totalHeightPx).fastCoerceIn(-topHeightPx, 0f) +} + +private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { + return heightOffset.fastCoerceIn(topHeightPx - totalHeightPx, 0f) +} + +private fun TopAppBarState.topCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float { + val offset = topHeightOffset(topHeightPx, totalHeightPx) + return offset / -topHeightPx +} + +private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float { + val offset = bottomHeightOffset(topHeightPx, totalHeightPx) + return offset / (topHeightPx - totalHeightPx) +} + +@Composable +fun enterAlwaysCollapsedScrollBehavior( + state: TopAppBarState = rememberTopAppBarState(), + canScroll: () -> Boolean = { true }, + isAtTop: () -> Boolean = { true }, + snapAnimationSpec: AnimationSpec? = spring(stiffness = Spring.StiffnessMediumLow), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay() +): TopAppBarScrollBehavior { + val (topHeightPx, totalHeightPx) = with(LocalDensity.current) { + CollapsedContainerHeight.toPx() to ExpandedContainerHeight.toPx() + } + + return remember(state, canScroll, isAtTop, snapAnimationSpec, flingAnimationSpec, topHeightPx, totalHeightPx) { + EnterAlwaysCollapsedScrollBehavior( + state = state, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + canScroll = canScroll, + isAtTop = isAtTop, + topHeightPx = topHeightPx, + totalHeightPx = totalHeightPx, + ) + } +} + +private class EnterAlwaysCollapsedScrollBehavior( + override val state: TopAppBarState, + override val snapAnimationSpec: AnimationSpec?, + override val flingAnimationSpec: DecayAnimationSpec?, + val canScroll: () -> Boolean = { true }, + // FIXME: See if it's possible to eliminate this argument + val isAtTop: () -> Boolean = { true }, + val topHeightPx: Float, + val totalHeightPx: Float, +) : TopAppBarScrollBehavior { + override val isPinned: Boolean = false + override var nestedScrollConnection = + object : NestedScrollConnection { + private fun TopAppBarState.setClampedOffsetIfAtTop(offset: Float) { + heightOffset = if (isAtTop()) { + offset + } else { + offset.fastCoerceIn(-totalHeightPx, (topHeightPx - totalHeightPx)) + } + } + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Don't intercept if scrolling down. + if (!canScroll() || (available.y > 0f && state.rawTopHeightOffset(topHeightPx, totalHeightPx) >= 0f)) + return Offset.Zero + + val prevHeightOffset = state.heightOffset + state.setClampedOffsetIfAtTop(state.heightOffset + available.y) + return if (prevHeightOffset != state.heightOffset) { + // We're in the middle of top app bar collapse or expand. + // Consume only the scroll on the Y axis. + available.copy(x = 0f) + } else { + Offset.Zero + } + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + if (!canScroll()) return Offset.Zero + state.contentOffset += consumed.y + + if (available.y < 0f || consumed.y < 0f) { + // When scrolling up, just update the state's height offset. + val oldHeightOffset = state.heightOffset + state.setClampedOffsetIfAtTop(state.heightOffset + consumed.y) + return Offset(0f, state.heightOffset - oldHeightOffset) + } + + if (consumed.y == 0f && available.y > 0) { + // Reset the total content offset to zero when scrolling all the way down. This + // will eliminate some float precision inaccuracies. + state.contentOffset = 0f + } + + if (available.y > 0f) { + // Adjust the height offset in case the consumed delta Y is less than what was + // recorded as available delta Y in the pre-scroll. + val oldHeightOffset = state.heightOffset + state.setClampedOffsetIfAtTop(state.heightOffset + available.y) + return Offset(0f, state.heightOffset - oldHeightOffset) + } + return Offset.Zero + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val superConsumed = super.onPostFling(consumed, available) + return superConsumed + + settleAppBar(state, available.y, topHeightPx, totalHeightPx, flingAnimationSpec, snapAnimationSpec) + } + } +} + +val CollapsedContainerHeight = 64.0.dp +val ExpandedContainerHeight = 152.0.dp +internal val TitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) +private val MediumTitleBottomPadding = 24.dp +private val LargeTitleBottomPadding = 28.dp +private val TopAppBarHorizontalPadding = 4.dp +private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding diff --git a/presentation/core/src/main/java/yokai/presentation/core/Scrollbar.kt b/presentation/core/src/main/java/yokai/presentation/core/Scrollbar.kt new file mode 100644 index 0000000000..12e30420bc --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/Scrollbar.kt @@ -0,0 +1,282 @@ +package yokai.presentation.core + +/* + * MIT License + * + * Copyright (c) 2022 Albert Chang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Code taken from https://gist.github.com/mxalbert1996/33a360fcab2105a31e5355af98216f5a + * with some modifications to handle contentPadding. + * + * Modifiers for regular scrollable list is omitted. + */ + +import android.view.ViewConfiguration +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +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.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirstOrNull +import androidx.compose.ui.util.fastSumBy +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import yokai.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX + +// FIXME: Scrollbar won't show up when TopAppBar is expanding/collapsing + +/** + * Draws horizontal scrollbar to a LazyList. + * + * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list. + */ +fun Modifier.drawHorizontalScrollbar( + state: LazyListState, + reverseScrolling: Boolean = false, + // The amount of offset the scrollbar position towards the top of the layout + positionOffsetPx: Float = 0f, +): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling, positionOffsetPx) + +/** + * Draws vertical scrollbar to a LazyList. + * + * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list. + */ +fun Modifier.drawVerticalScrollbar( + state: LazyListState, + reverseScrolling: Boolean = false, + // The amount of offset the scrollbar position towards the start of the layout + positionOffsetPx: Float = 0f, +): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling, positionOffsetPx) + +private fun Modifier.drawScrollbar( + state: LazyListState, + orientation: Orientation, + reverseScrolling: Boolean, + positionOffset: Float, +): Modifier = drawScrollbar( + orientation, + reverseScrolling, +) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = if (orientation == Orientation.Horizontal) { + layoutInfo.viewportSize.width + } else { + layoutInfo.viewportSize.height + } - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding + val items = layoutInfo.visibleItemsInfo + val itemsSize = items.fastSumBy { it.size } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val thumbSize = viewportSize / totalSize * viewportSize + val startOffset = if (items.isEmpty()) { + 0f + } else { + items + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true } + ?.run { + val startPadding = if (reverseDirection) { + layoutInfo.afterContentPadding + } else { + layoutInfo.beforeContentPadding + } + startPadding + ((estimatedItemSize * index - offset) / totalSize * viewportSize) + } ?: 0f + } + val drawScrollbar = onDrawScrollbar( + orientation, reverseDirection, atEnd, showScrollbar, + thickness, color, alpha, thumbSize, startOffset, positionOffset, + ) + drawContent() + drawScrollbar() +} + +private fun ContentDrawScope.onDrawScrollbar( + orientation: Orientation, + reverseDirection: Boolean, + atEnd: Boolean, + showScrollbar: Boolean, + thickness: Float, + color: Color, + alpha: () -> Float, + thumbSize: Float, + scrollOffset: Float, + positionOffset: Float, +): DrawScope.() -> Unit { + val topLeft = if (orientation == Orientation.Horizontal) { + Offset( + if (reverseDirection) size.width - scrollOffset - thumbSize else scrollOffset, + if (atEnd) size.height - positionOffset - thickness else positionOffset, + ) + } else { + Offset( + if (atEnd) size.width - positionOffset - thickness else positionOffset, + if (reverseDirection) size.height - scrollOffset - thumbSize else scrollOffset, + ) + } + val size = if (orientation == Orientation.Horizontal) { + Size(thumbSize, thickness) + } else { + Size(thickness, thumbSize) + } + + return { + if (showScrollbar) { + drawRect( + color = color, + topLeft = topLeft, + size = size, + alpha = alpha(), + ) + } + } +} + +private fun Modifier.drawScrollbar( + orientation: Orientation, + reverseScrolling: Boolean, + onDraw: ContentDrawScope.( + reverseDirection: Boolean, + atEnd: Boolean, + thickness: Float, + color: Color, + alpha: () -> Float, + ) -> Unit, +): Modifier = composed { + val scrolled = remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + } + val nestedScrollConnection = remember(orientation, scrolled) { + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y + if (delta != 0f) scrolled.tryEmit(Unit) + return Offset.Zero + } + } + } + + val alpha = remember { Animatable(0f) } + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } + } + + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val reverseDirection = if (orientation == Orientation.Horizontal) { + if (isLtr) reverseScrolling else !reverseScrolling + } else { + reverseScrolling + } + val atEnd = if (orientation == Orientation.Vertical) isLtr else true + + val context = LocalContext.current + val thickness = remember { ViewConfiguration.get(context).scaledScrollBarSize.toFloat() } + val color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.364f) + Modifier + .nestedScroll(nestedScrollConnection) + .drawWithContent { + onDraw(reverseDirection, atEnd, thickness, color, alpha::value) + } +} + +private val FadeOutAnimationSpec = tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), + delayMillis = ViewConfiguration.getScrollDefaultDelay(), +) + +@Preview(widthDp = 400, heightDp = 400, showBackground = true) +@Composable +fun LazyListScrollbarPreview() { + val state = rememberLazyListState() + LazyColumn( + modifier = Modifier.drawVerticalScrollbar(state), + state = state, + ) { + items(50) { + Text( + text = "Item ${it + 1}", + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } + } +} + +@Preview(widthDp = 400, showBackground = true) +@Composable +fun LazyListHorizontalScrollbarPreview() { + val state = rememberLazyListState() + LazyRow( + modifier = Modifier.drawHorizontalScrollbar(state), + state = state, + ) { + items(50) { + Text( + text = (it + 1).toString(), + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 16.dp), + ) + } + } +} diff --git a/presentation/core/src/main/java/yokai/presentation/core/components/LazyGrid.kt b/presentation/core/src/main/java/yokai/presentation/core/components/LazyGrid.kt new file mode 100644 index 0000000000..1532c990cc --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/components/LazyGrid.kt @@ -0,0 +1,58 @@ +package yokai.presentation.core.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun FastScrollLazyVerticalGrid( + columns: GridCells, + modifier: Modifier = Modifier, + state: LazyGridState = rememberLazyGridState(), + thumbAllowed: () -> Boolean = { true }, + thumbColor: Color = MaterialTheme.colorScheme.primary, + contentPadding: PaddingValues = PaddingValues(0.dp), + topContentPadding: Dp = Dp.Hairline, + bottomContentPadding: Dp = Dp.Hairline, + endContentPadding: Dp = Dp.Hairline, + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + userScrollEnabled: Boolean = true, + content: LazyGridScope.() -> Unit, +) { + VerticalGridFastScroller( + state = state, + columns = columns, + arrangement = horizontalArrangement, + contentPadding = contentPadding, + modifier = modifier, + thumbAllowed = thumbAllowed, + thumbColor = thumbColor, + topContentPadding = topContentPadding, + bottomContentPadding = bottomContentPadding, + endContentPadding = endContentPadding, + ) { + LazyVerticalGrid( + columns = columns, + state = state, + contentPadding = contentPadding, + reverseLayout = reverseLayout, + verticalArrangement = verticalArrangement, + horizontalArrangement = horizontalArrangement, + userScrollEnabled = userScrollEnabled, + content = content, + ) + } +} diff --git a/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt b/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt new file mode 100644 index 0000000000..03d02b421d --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/components/LinkIcon.kt @@ -0,0 +1,31 @@ +package yokai.presentation.core.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp + +@Composable +fun LinkIcon( + label: String, + icon: ImageVector, + url: String, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + IconButton( + modifier = modifier.padding(4.dp), + onClick = { uriHandler.openUri(url) }, + ) { + Icon( + imageVector = icon, + tint = MaterialTheme.colorScheme.primary, + contentDescription = label, + ) + } +} diff --git a/presentation/core/src/main/java/yokai/presentation/core/components/VerticalFastScroller.kt b/presentation/core/src/main/java/yokai/presentation/core/components/VerticalFastScroller.kt new file mode 100644 index 0000000000..27221e5e64 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/components/VerticalFastScroller.kt @@ -0,0 +1,448 @@ +package yokai.presentation.core.components + +import android.view.ViewConfiguration +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.systemGestureExclusion +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirstOrNull +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMaxBy +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import yokai.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX + +/** + * Draws vertical fast scroller to a lazy list + * + * Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list. + */ +@Composable +fun VerticalFastScroller( + listState: LazyListState, + modifier: Modifier = Modifier, + thumbAllowed: () -> Boolean = { true }, + thumbColor: Color = MaterialTheme.colorScheme.primary, + topContentPadding: Dp = Dp.Hairline, + bottomContentPadding: Dp = Dp.Hairline, + endContentPadding: Dp = Dp.Hairline, + content: @Composable () -> Unit, +) { + SubcomposeLayout(modifier = modifier) { constraints -> + val contentPlaceable = subcompose("content", content).map { it.measure(constraints) } + val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0 + val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0 + + val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val scrollerPlaceable = subcompose("scroller") { + val layoutInfo = listState.layoutInfo + val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount + if (!showScroller) return@subcompose + + val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } + var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } + + val dragInteractionSource = remember { MutableInteractionSource() } + val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() + val scrolled = remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + } + + val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } + val heightPx = contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + listState.layoutInfo.afterContentPadding + val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } + val trackHeightPx = heightPx - thumbHeightPx + + // When thumb dragged + LaunchedEffect(thumbOffsetY) { + if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect + val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx + val scrollItem = layoutInfo.totalItemsCount * scrollRatio + val scrollItemRounded = scrollItem.roundToInt() + val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0 + val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded) + listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt()) + scrolled.tryEmit(Unit) + } + + // When list scrolled + LaunchedEffect(listState.firstVisibleItemScrollOffset) { + if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect + val scrollOffset = computeScrollOffset(state = listState) + val scrollRange = computeScrollRange(state = listState) + val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) + thumbOffsetY = trackHeightPx * proportion + thumbTopPadding + scrolled.tryEmit(Unit) + } + + // Thumb alpha + val alpha = remember { Animatable(0f) } + val isThumbVisible = alpha.value > 0f + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + if (thumbAllowed()) { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } else { + alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + } + } + } + + Box( + modifier = Modifier + .offset { IntOffset(0, thumbOffsetY.roundToInt()) } + .then( + // Recompose opts + if (isThumbVisible && !listState.isScrollInProgress) { + Modifier.draggable( + interactionSource = dragInteractionSource, + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + val newOffsetY = thumbOffsetY + delta + thumbOffsetY = newOffsetY.coerceIn( + thumbTopPadding, + thumbTopPadding + trackHeightPx, + ) + }, + ) + } else { + Modifier + }, + ) + .then( + // Exclude thumb from gesture area only when needed + if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) { + Modifier.systemGestureExclusion() + } else { + Modifier + }, + ) + .height(ThumbLength) + .padding(horizontal = 8.dp) + .padding(end = endContentPadding) + .width(ThumbThickness) + .alpha(alpha.value) + .background(color = thumbColor, shape = ThumbShape), + ) + }.map { it.measure(scrollerConstraints) } + val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0 + + layout(contentWidth, contentHeight) { + contentPlaceable.fastForEach { + it.place(0, 0) + } + scrollerPlaceable.fastForEach { + it.placeRelative(contentWidth - scrollerWidth, 0) + } + } + } +} + +@Composable +private fun rememberColumnWidthSums( + columns: GridCells, + horizontalArrangement: Arrangement.Horizontal, + contentPadding: PaddingValues, +) = remember List>( + columns, + horizontalArrangement, + contentPadding, +) { + { constraints -> + require(constraints.maxWidth != Constraints.Infinity) { + "LazyVerticalGrid's width should be bound by parent" + } + val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) + + contentPadding.calculateEndPadding(LayoutDirection.Ltr) + val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx() + with(columns) { + calculateCrossAxisCellSizes( + gridWidth, + horizontalArrangement.spacing.roundToPx(), + ).toMutableList().apply { + for (i in 1.. Boolean = { true }, + thumbColor: Color = MaterialTheme.colorScheme.primary, + topContentPadding: Dp = Dp.Hairline, + bottomContentPadding: Dp = Dp.Hairline, + endContentPadding: Dp = Dp.Hairline, + content: @Composable () -> Unit, +) { + val slotSizesSums = rememberColumnWidthSums( + columns = columns, + horizontalArrangement = arrangement, + contentPadding = contentPadding, + ) + + SubcomposeLayout(modifier = modifier) { constraints -> + val contentPlaceable = subcompose("content", content).map { it.measure(constraints) } + val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0 + val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0 + + val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val scrollerPlaceable = subcompose("scroller") { + val layoutInfo = state.layoutInfo + val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount + if (!showScroller) return@subcompose + val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() } + var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) } + + val dragInteractionSource = remember { MutableInteractionSource() } + val isThumbDragged by dragInteractionSource.collectIsDraggedAsState() + val scrolled = remember { + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + } + + val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() } + val heightPx = contentHeight.toFloat() - + thumbTopPadding - + thumbBottomPadding - + state.layoutInfo.afterContentPadding + val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() } + val trackHeightPx = heightPx - thumbHeightPx + + val columnCount = remember { slotSizesSums(constraints).size } + + // When thumb dragged + LaunchedEffect(thumbOffsetY) { + if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect + val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx + val scrollItem = layoutInfo.totalItemsCount * scrollRatio + // I can't think of anything else rn but this'll do + val scrollItemWhole = scrollItem.toInt() + val columnNum = ((scrollItemWhole + 1) % columnCount).takeIf { it != 0 } ?: columnCount + val scrollItemFraction = if (scrollItemWhole == 0) scrollItem else scrollItem % scrollItemWhole + val offsetPerItem = 1f / columnCount + val offsetRatio = (offsetPerItem * scrollItemFraction) + (offsetPerItem * (columnNum - 1)) + + // TODO: Sometimes item height is not available when scrolling up + val scrollItemSize = (1..columnCount).maxOf { num -> + val actualIndex = if (num != columnNum) { + scrollItemWhole + num - columnCount + } else { + scrollItemWhole + } + layoutInfo.visibleItemsInfo.find { it.index == actualIndex }?.size?.height ?: 0 + } + val scrollItemOffset = scrollItemSize * offsetRatio + + state.scrollToItem(index = scrollItemWhole, scrollOffset = scrollItemOffset.roundToInt()) + scrolled.tryEmit(Unit) + } + + // When list scrolled + LaunchedEffect(state.firstVisibleItemScrollOffset) { + if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect + val scrollOffset = computeScrollOffset(state = state) + val scrollRange = computeScrollRange(state = state) + val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx) + thumbOffsetY = trackHeightPx * proportion + thumbTopPadding + scrolled.tryEmit(Unit) + } + + // Thumb alpha + val alpha = remember { Animatable(0f) } + val isThumbVisible = alpha.value > 0f + LaunchedEffect(scrolled, alpha) { + scrolled + .sample(100) + .collectLatest { + if (thumbAllowed()) { + alpha.snapTo(1f) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } else { + alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec) + } + } + } + + Box( + modifier = Modifier + .offset { IntOffset(0, thumbOffsetY.roundToInt()) } + .then( + // Recompose opts + if (isThumbVisible && !state.isScrollInProgress) { + Modifier.draggable( + interactionSource = dragInteractionSource, + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + val newOffsetY = thumbOffsetY + delta + thumbOffsetY = newOffsetY.coerceIn( + thumbTopPadding, + thumbTopPadding + trackHeightPx, + ) + }, + ) + } else { + Modifier + }, + ) + .then( + // Exclude thumb from gesture area only when needed + if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) { + Modifier.systemGestureExclusion() + } else { + Modifier + }, + ) + .height(ThumbLength) + .padding(end = endContentPadding) + .width(ThumbThickness) + .alpha(alpha.value) + .background(color = thumbColor, shape = ThumbShape), + ) + }.map { it.measure(scrollerConstraints) } + val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0 + + layout(contentWidth, contentHeight) { + contentPlaceable.fastForEach { + it.place(0, 0) + } + scrollerPlaceable.fastForEach { + it.placeRelative(contentWidth - scrollerWidth, 0) + } + } + } +} + +private fun computeScrollOffset(state: LazyGridState): Int { + if (state.layoutInfo.totalItemsCount == 0) return 0 + val visibleItems = state.layoutInfo.visibleItemsInfo + val startChild = visibleItems.first() + val endChild = visibleItems.last() + val minPosition = min(startChild.index, endChild.index) + val maxPosition = max(startChild.index, endChild.index) + val itemsBefore = minPosition.coerceAtLeast(0) + val startDecoratedTop = startChild.offset.y + val laidOutArea = abs((endChild.offset.y + endChild.size.height) - startDecoratedTop) + val itemRange = abs(minPosition - maxPosition) + 1 + val avgSizePerRow = laidOutArea.toFloat() / itemRange + return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt() +} + +private fun computeScrollRange(state: LazyGridState): Int { + if (state.layoutInfo.totalItemsCount == 0) return 0 + val visibleItems = state.layoutInfo.visibleItemsInfo + val startChild = visibleItems.first() + val endChild = visibleItems.last() + val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y + val laidOutRange = abs(startChild.index - endChild.index) + 1 + return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt() +} + +private fun computeScrollOffset(state: LazyListState): Int { + if (state.layoutInfo.totalItemsCount == 0) return 0 + val visibleItems = state.layoutInfo.visibleItemsInfo + val startChild = visibleItems + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! + val endChild = visibleItems.last() + val minPosition = min(startChild.index, endChild.index) + val maxPosition = max(startChild.index, endChild.index) + val itemsBefore = minPosition.coerceAtLeast(0) + val startDecoratedTop = startChild.top + val laidOutArea = abs(endChild.bottom - startDecoratedTop) + val itemRange = abs(minPosition - maxPosition) + 1 + val avgSizePerRow = laidOutArea.toFloat() / itemRange + return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt() +} + +private fun computeScrollRange(state: LazyListState): Int { + if (state.layoutInfo.totalItemsCount == 0) return 0 + val visibleItems = state.layoutInfo.visibleItemsInfo + val startChild = visibleItems + .fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!! + val endChild = visibleItems.last() + val laidOutArea = endChild.bottom - startChild.top + val laidOutRange = abs(startChild.index - endChild.index) + 1 + return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt() +} + +object Scroller { + const val STICKY_HEADER_KEY_PREFIX = "sticky:" +} + +private val ThumbLength = 48.dp +private val ThumbThickness = 12.dp +private val ThumbShape = RoundedCornerShape(ThumbThickness / 2) +private val FadeOutAnimationSpec = tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), + delayMillis = 2000, +) +private val ImmediateFadeOutAnimationSpec = tween( + durationMillis = ViewConfiguration.getScrollBarFadeDuration(), +) + +private val LazyListItemInfo.top: Int + get() = offset + +private val LazyListItemInfo.bottom: Int + get() = offset + size diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt new file mode 100644 index 0000000000..6b6ce894a2 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/CustomIcons.kt @@ -0,0 +1,3 @@ +package yokai.presentation.core.icons + +object CustomIcons diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt new file mode 100644 index 0000000000..5eb4d368ee --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/Discord.kt @@ -0,0 +1,86 @@ +package yokai.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("UnusedReceiverParameter", "BooleanLiteralArgument") +val CustomIcons.Discord: ImageVector + get() { + if (_discord != null) { + return _discord!! + } + _discord = Builder( + name = "Discord", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(20.317f, 4.3698f) + arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f) + arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f) + curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f) + curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f) + curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f) + arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f) + arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f) + curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f) + arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f) + curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f) + arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f) + curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f) + curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f) + curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f) + arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f) + curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f) + arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f) + curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f) + arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f) + arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f) + curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f) + curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f) + curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f) + arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f) + close() + moveTo(8.02f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f) + close() + moveTo(15.9948f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f) + close() + } + } + .build() + return _discord!! + } + +@Suppress("ObjectPropertyName") +private var _discord: ImageVector? = null diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt new file mode 100644 index 0000000000..5115f5d0ff --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/GitHub.kt @@ -0,0 +1,68 @@ +package yokai.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("UnusedReceiverParameter") +val CustomIcons.GitHub: ImageVector + get() { + if (_github != null) { + return _github!! + } + _github = Builder( + name = "GitHub", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12.0f, 0.297f) + curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f) + curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f) + curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f) + curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f) + curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f) + curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f) + curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f) + curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f) + curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f) + curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f) + curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f) + curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f) + curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f) + curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f) + curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f) + curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f) + curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f) + curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f) + curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f) + curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f) + curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f) + curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f) + curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f) + curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f) + curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) + } + } + .build() + return _github!! + } + +@Suppress("ObjectPropertyName") +private var _github: ImageVector? = null diff --git a/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt b/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt new file mode 100644 index 0000000000..b45c727597 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/icons/LocalSource.kt @@ -0,0 +1,56 @@ +package yokai.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("UnusedReceiverParameter") +val CustomIcons.LocalSource: ImageVector + get() { + if (_localSource != null) { + return _localSource!! + } + _localSource = Builder( + name = "localSource", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), + stroke = null, + strokeLineWidth = 0.0f, + strokeLineCap = Butt, + strokeLineJoin = Miter, + strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12f, 11.55f) + curveTo(9.64f, 9.35f, 6.48f, 8f, 3f, 8f) + verticalLineToRelative(11f) + curveToRelative(3.48f, 0f, 6.64f, 1.35f, 9f, 3.55f) + curveToRelative(2.36f, -2.19f, 5.52f, -3.55f, 9f, -3.55f) + verticalLineTo(8f) + curveToRelative(-3.48f, 0f, -6.64f, 1.35f, -9f, 3.55f) + close() + moveTo(12f, 8f) + curveToRelative(1.66f, 0f, 3f, -1.34f, 3f, -3f) + reflectiveCurveToRelative(-1.34f, -3f, -3f, -3f) + reflectiveCurveToRelative(-3f, 1.34f, -3f, 3f) + reflectiveCurveToRelative(1.34f, 3f, 3f, 3f) + close() + } + } + .build() + return _localSource!! + } + +@Suppress("ObjectPropertyName") +private var _localSource: ImageVector? = null diff --git a/presentation/core/src/main/java/yokai/presentation/core/util/PaddingValues.kt b/presentation/core/src/main/java/yokai/presentation/core/util/PaddingValues.kt new file mode 100644 index 0000000000..8422a88ec4 --- /dev/null +++ b/presentation/core/src/main/java/yokai/presentation/core/util/PaddingValues.kt @@ -0,0 +1,22 @@ +package yokai.presentation.core.util + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +@ReadOnlyComposable +operator fun PaddingValues.plus(other: PaddingValues): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + return PaddingValues( + start = calculateStartPadding(layoutDirection) + + other.calculateStartPadding(layoutDirection), + end = calculateEndPadding(layoutDirection) + + other.calculateEndPadding(layoutDirection), + top = calculateTopPadding() + other.calculateTopPadding(), + bottom = calculateBottomPadding() + other.calculateBottomPadding(), + ) +} diff --git a/presentation/widget/build.gradle.kts b/presentation/widget/build.gradle.kts index 4f4f4edb44..def28f0709 100644 --- a/presentation/widget/build.gradle.kts +++ b/presentation/widget/build.gradle.kts @@ -1,9 +1,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(androidx.plugins.library) - alias(kotlinx.plugins.android) - alias(kotlinx.plugins.compose.compiler) + id("yokai.android.library") + id("yokai.android.library.compose") + kotlin("android") } android { @@ -20,7 +20,7 @@ android { } dependencies { - implementation(projects.core) + implementation(projects.core.main) implementation(projects.data) implementation(projects.domain) implementation(projects.i18n) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1c0f8209f0..68a592a5f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ dependencyResolutionManagement { google() maven("https://jitpack.io") maven("https://plugins.gradle.org/m2/") + maven("https://s01.oss.sonatype.org/content/repositories/releases/") } } @@ -31,7 +32,8 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "Yokai" include(":app") -include(":core") +include(":core:archive") +include(":core:main") include(":data") include(":domain") include(":i18n") diff --git a/source/api/build.gradle.kts b/source/api/build.gradle.kts index 8d6fe1986a..3e4b8dac0e 100644 --- a/source/api/build.gradle.kts +++ b/source/api/build.gradle.kts @@ -1,8 +1,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(androidx.plugins.library) - alias(kotlinx.plugins.multiplatform) + id("yokai.android.library") + kotlin("multiplatform") alias(kotlinx.plugins.serialization) } @@ -21,7 +21,7 @@ kotlin { } val androidMain by getting { dependencies { - implementation(projects.core) + implementation(projects.core.main) api(androidx.preference) // Workaround for https://youtrack.jetbrains.com/issue/KT-57605 diff --git a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt index 7ce18934f3..ce350c615c 100644 --- a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt +++ b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt @@ -19,7 +19,7 @@ open class Page( get() = index + 1 @Transient - private val _statusFlow = MutableStateFlow(State.QUEUE) + private val _statusFlow = MutableStateFlow(State.Queue) @Transient val statusFlow = _statusFlow.asStateFlow() @@ -48,11 +48,11 @@ open class Page( } } - enum class State { - QUEUE, - LOAD_PAGE, - DOWNLOAD_IMAGE, - READY, - ERROR, + sealed interface State { + data object Queue : State + data object LoadPage : State + data object DownloadImage : State + data object Ready : State + data object Error : State } } diff --git a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt new file mode 100644 index 0000000000..cbcf769461 --- /dev/null +++ b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt @@ -0,0 +1,378 @@ +package eu.kanade.tachiyomi.source.online + +import android.net.Uri +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable + +abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() { + /** + * Returns the request for the popular manga given the page. + * + * @param page the page number to retrieve. + */ + override fun popularMangaRequest(page: Int) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun popularMangaParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Returns the request for the search manga given the page. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun searchMangaParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Returns the request for latest manga given the page. + * + * @param page the page number to retrieve. + */ + override fun latestUpdatesRequest(page: Int) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. + */ + override fun latestUpdatesParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns the details of a manga. + * + * @param response the response from the site. + */ + override fun mangaDetailsParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a list of chapters. + * + * @param response the response from the site. + */ + override fun chapterListParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a SChapter Object. + * + * @param response the response from the site. + */ + override fun chapterPageParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns a list of pages. + * + * @param response the response from the site. + */ + override fun pageListParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + /** + * Parses the response from the site and returns the absolute url to the source image. + * + * @param response the response from the site. + */ + override fun imageUrlParse(response: Response) = + throw UnsupportedOperationException("Should never be called!") + + abstract val domainName: String + + /** + * Base url of the website without the trailing slash, like: http://mysite.com + */ + override val baseUrl get() = delegate.baseUrl + + /** + * Headers used for requests. + */ + override val headers get() = delegate.headers + + /** + * Whether the source has support for latest updates. + */ + override val supportsLatest get() = delegate.supportsLatest + + /** + * Name of the source. + */ + final override val name get() = delegate.name + + // ===> OPTIONAL FIELDS + + /** + * Id of the source. By default it uses a generated id using the first 16 characters (64 bits) + * of the MD5 of the string: sourcename/language/versionId + * Note the generated id sets the sign bit to 0. + */ + override val id get() = delegate.id + + /** + * Default network client for doing requests. + */ + final override val client get() = delegate.client + + /** + * You must NEVER call super.client if you override this! + */ + open val baseHttpClient: OkHttpClient? = null + open val networkHttpClient: OkHttpClient get() = network.client + + /** + * Visible name of the source. + */ + override fun toString() = delegate.toString() + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga")) + override fun fetchPopularManga(page: Int): Observable { + ensureDelegateCompatible() + return delegate.fetchPopularManga(page) + } + + override suspend fun getPopularManga(page: Int): MangasPage { + ensureDelegateCompatible() + return delegate.getPopularManga(page) + } + + /** + * Returns an observable containing a page with a list of manga. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + ensureDelegateCompatible() + return delegate.fetchSearchManga(page, query, filters) + } + + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { + ensureDelegateCompatible() + return delegate.getSearchManga(page, query, filters) + } + + /** + * Returns an observable containing a page with a list of latest manga updates. + * + * @param page the page number to retrieve. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates")) + override fun fetchLatestUpdates(page: Int): Observable { + ensureDelegateCompatible() + return delegate.fetchLatestUpdates(page) + } + + override suspend fun getLatestUpdates(page: Int): MangasPage { + ensureDelegateCompatible() + return delegate.getLatestUpdates(page) + } + + /** + * Returns an observable with the updated details for a manga. Normally it's not needed to + * override this method. + * + * @param manga the manga to be updated. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) + override fun fetchMangaDetails(manga: SManga): Observable { + ensureDelegateCompatible() + return delegate.fetchMangaDetails(manga) + } + + /** + * [1.x API] Get the updated details for a manga. + */ + override suspend fun getMangaDetails(manga: SManga): SManga { + ensureDelegateCompatible() + return delegate.getMangaDetails(manga) + } + + /** + * Returns the request for the details of a manga. Override only if it's needed to change the + * url, send different headers or request method like POST. + * + * @param manga the manga to be updated. + */ + override fun mangaDetailsRequest(manga: SManga): Request { + ensureDelegateCompatible() + return delegate.mangaDetailsRequest(manga) + } + + /** + * Returns an observable with the updated chapter list for a manga. Normally it's not needed to + * override this method. If a manga is licensed an empty chapter list observable is returned + * + * @param manga the manga to look for chapters. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) + override fun fetchChapterList(manga: SManga): Observable> { + ensureDelegateCompatible() + return delegate.fetchChapterList(manga) + } + + /** + * [1.x API] Get all the available chapters for a manga. + */ + override suspend fun getChapterList(manga: SManga): List { + ensureDelegateCompatible() + return delegate.getChapterList(manga) + } + + /** + * Returns an observable with the page list for a chapter. + * + * @param chapter the chapter whose page list has to be fetched. + */ + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList")) + override fun fetchPageList(chapter: SChapter): Observable> { + ensureDelegateCompatible() + return delegate.fetchPageList(chapter) + } + + /** + * [1.x API] Get the list of pages a chapter has. + */ + override suspend fun getPageList(chapter: SChapter): List { + ensureDelegateCompatible() + return delegate.getPageList(chapter) + } + + /** + * Returns an observable with the page containing the source url of the image. If there's any + * error, it will return null instead of throwing an exception. + * + * @param page the page whose source image has to be fetched. + */ + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) + override fun fetchImageUrl(page: Page): Observable { + ensureDelegateCompatible() + return delegate.fetchImageUrl(page) + } + + override suspend fun getImageUrl(page: Page): String { + ensureDelegateCompatible() + return delegate.getImageUrl(page) + } + + /** + * Returns the response of the source image. + * + * @param page the page whose source image has to be downloaded. + */ + override suspend fun getImage(page: Page): Response { + ensureDelegateCompatible() + return delegate.getImage(page) + } + + /** + * Returns the url of the provided manga + * + * @since extensions-lib 1.4 + * @param manga the manga + * @return url of the manga + */ + override fun getMangaUrl(manga: SManga): String { + ensureDelegateCompatible() + return delegate.getMangaUrl(manga) + } + + /** + * Returns the url of the provided chapter + * + * @since extensions-lib 1.4 + * @param chapter the chapter + * @return url of the chapter + */ + override fun getChapterUrl(chapter: SChapter): String { + ensureDelegateCompatible() + return delegate.getChapterUrl(chapter) + } + + /** + * Called before inserting a new chapter into database. Use it if you need to override chapter + * fields, like the title or the chapter number. Do not change anything to [manga]. + * + * @param chapter the chapter to be added. + * @param manga the manga of the chapter. + */ + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + ensureDelegateCompatible() + return delegate.prepareNewChapter(chapter, manga) + } + + /** + * Returns the list of filters for the source. + */ + override fun getFilterList() = delegate.getFilterList() + + abstract fun canOpenUrl(uri: Uri): Boolean + abstract fun chapterUrl(uri: Uri): String? + open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull() + abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? + + open suspend fun getMangaDetailsByUrl(url: String): SManga { + val manga = SManga.create().apply { + this.url = url + this.title = "" + } + return delegate.getMangaDetails(manga.copy()) + } + + open suspend fun getChapterListByUrl(url: String): List { + val manga = SManga.create().apply { + this.url = url + this.title = "" + } + return delegate.getChapterList(manga) + } + + protected open fun ensureDelegateCompatible() { + if (versionId != delegate.versionId || lang != delegate.lang) { + throw IncompatibleDelegateException( + "Delegate source is not compatible (" + + "versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang}" + + ")!", + ) + } + } + + class IncompatibleDelegateException(message: String) : RuntimeException(message) + + init { + delegate.bindDelegate(this) + } +} diff --git a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 05494e6062..4faaffe463 100644 --- a/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source/api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -12,21 +12,22 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.awaitSingle +import java.net.URI +import java.net.URISyntaxException +import java.security.MessageDigest import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable import uy.kohesive.injekt.injectLazy -import java.net.URI -import java.net.URISyntaxException -import java.security.MessageDigest /** * A simple implementation for sources from a website. */ @Suppress("unused") abstract class HttpSource : CatalogueSource { + private var delegate: DelegatedHttpSource? = null /** * Network service. @@ -59,7 +60,7 @@ abstract class HttpSource : CatalogueSource { /** * Headers used for requests. */ - val headers: Headers by lazy { headersBuilder().build() } + open val headers: Headers by lazy { headersBuilder().build() } /** * Default network client for doing requests. @@ -67,6 +68,10 @@ abstract class HttpSource : CatalogueSource { open val client: OkHttpClient get() = network.client + fun bindDelegate(delegate: DelegatedHttpSource) { + this.delegate = delegate + } + /** * Generates a unique ID for the source based on the provided [name], [lang] and * [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string @@ -283,6 +288,13 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun chapterListParse(response: Response): List + /** + * Parses the response from the site and returns a SChapter Object. + * + * @param response the response from the site. + */ + protected abstract fun chapterPageParse(response: Response): SChapter + /** * Get the list of pages a chapter has. Pages should be returned * in the expected order; the index is ignored.