diff --git a/.github/.java-version b/.github/.java-version new file mode 100644 index 000000000..aabe6ec39 --- /dev/null +++ b/.github/.java-version @@ -0,0 +1 @@ +21 diff --git a/.github/workflows/build_benchmark.yml b/.github/workflows/build_benchmark.yml old mode 100755 new mode 100644 index 81d347737..6f8216d6a --- a/.github/workflows/build_benchmark.yml +++ b/.github/workflows/build_benchmark.yml @@ -29,7 +29,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -57,18 +57,18 @@ jobs: needs: prepare-build steps: - name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v4 # Forgejo-compatible (node20) + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Set up gradle - uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -101,7 +101,7 @@ jobs: mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-benchmark-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk - name: Upload artifacts - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 30 diff --git a/.github/workflows/build_dispatch_preview.yml b/.github/workflows/build_dispatch_preview.yml old mode 100755 new mode 100644 index dad856562..92ea4eb94 --- a/.github/workflows/build_dispatch_preview.yml +++ b/.github/workflows/build_dispatch_preview.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -53,7 +53,7 @@ jobs: uses: tj-actions/branch-names@5250492686b253f06fa55861556d1027b067aeb5 # v9.0.2 - name: Invoke workflow in preview repo - uses: benc-uk/workflow-dispatch@v1.2.2 # Forgejo-compatible (node16) + uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 with: workflow: build_app.yml repo: komikku-app/komikku-preview diff --git a/.github/workflows/build_preview.yml b/.github/workflows/build_preview.yml old mode 100755 new mode 100644 index a201679ff..3541a7657 --- a/.github/workflows/build_preview.yml +++ b/.github/workflows/build_preview.yml @@ -43,7 +43,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -119,14 +119,14 @@ jobs: if: github.ref == 'refs/heads/master' steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }} - ${{ needs.prepare-build.outputs.TAG_NAME }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v4 # Forgejo-compatible (node20) + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json @@ -144,7 +144,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -156,7 +156,7 @@ jobs: run: ./gradlew testReleaseUnitTest - name: Upload artifacts - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 1 @@ -168,7 +168,7 @@ jobs: - build-app steps: - name: Download artifacts - uses: actions/download-artifact@v4 # Forgejo-compatible (node20) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true @@ -195,7 +195,7 @@ jobs: mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-preview-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk - name: Create release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }} name: Komikku Preview ${{ needs.prepare-build.outputs.TAG_NAME }} (${{ needs.prepare-build.outputs.VERSION_TAG }}) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml old mode 100755 new mode 100644 index 8bdad8592..c501815c1 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -30,20 +30,20 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Dependency Review - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - name: Set up JDK - uses: actions/setup-java@v4 # Forgejo-compatible (node20) + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json @@ -63,7 +63,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -82,7 +82,7 @@ jobs: - name: Upload test report if: steps.unit_tests.outcome == 'failure' - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-report-${{ github.sha }} path: app/build/reports/tests/testReleaseUnitTest @@ -119,13 +119,13 @@ jobs: mv app/build/outputs/apk/preview/app-universal-preview${{ steps.signed_filename.outputs.SIGNED_TRAIL }}.apk Komikku-$version_tag-r$commit_count.apk - name: Upload APK - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk path: ./*.apk - name: Upload mapping - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/preview diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml old mode 100755 new mode 100644 index 11dbf4233..e90e9f0c4 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -27,14 +27,14 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v4 # Forgejo-compatible (node20) + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json @@ -52,7 +52,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -66,7 +66,7 @@ jobs: - name: Upload test report if: steps.unit_tests.outcome == 'failure' - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-report-${{ github.sha }} path: app/build/reports/tests/testReleaseUnitTest @@ -95,13 +95,13 @@ jobs: mv app/build/outputs/apk/preview/app-universal-preview-signed.apk Komikku-$version_tag-r$commit_count.apk - name: Upload APK - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk path: ./*.apk - name: Upload mapping - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/preview diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml old mode 100755 new mode 100644 index e3aa716f4..4c562f21d --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -31,7 +31,7 @@ jobs: echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT - name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -87,14 +87,14 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }}) - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v4 # Forgejo-compatible (node20) + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - java-version: 17 + java-version-file: .github/.java-version distribution: temurin - name: Write google-services.json @@ -112,7 +112,7 @@ jobs: write-mode: overwrite - name: Set up gradle - uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20) + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6 - name: Check code format run: ./gradlew spotlessCheck @@ -124,13 +124,13 @@ jobs: run: ./gradlew testReleaseUnitTest - name: Upload artifacts - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: "**/*.apk" retention-days: 1 - name: Upload mapping - uses: actions/upload-artifact@v4 # Forgejo-compatible (node20) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/release @@ -142,7 +142,7 @@ jobs: - build-app steps: - name: Download artifacts - uses: actions/download-artifact@v4 # Forgejo-compatible (node20) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true @@ -169,7 +169,7 @@ jobs: mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.VERSION_TAG }}.apk - name: Create release - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }} name: Komikku ${{ needs.prepare-build.outputs.VERSION_TAG }} diff --git a/.github/workflows/codeberg_mirror.yml b/.github/workflows/codeberg_mirror.yml old mode 100755 new mode 100644 index ba036e0cf..59ff40c15 --- a/.github/workflows/codeberg_mirror.yml +++ b/.github/workflows/codeberg_mirror.yml @@ -13,7 +13,7 @@ jobs: if: github.repository == 'komikku-app/komikku' runs-on: 'ubuntu-24.04' steps: - - uses: actions/checkout@v4 # Forgejo-compatible (node20) + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Setup SSH key diff --git a/.github/workflows/delete_merged_branch.yml b/.github/workflows/delete_merged_branch.yml old mode 100755 new mode 100644 index ad1c96717..6cbf2a266 --- a/.github/workflows/delete_merged_branch.yml +++ b/.github/workflows/delete_merged_branch.yml @@ -13,7 +13,7 @@ jobs: if: github.event.pull_request.merged == true && github.repository == github.event.pull_request.head.repo.full_name steps: - name: Checkout repository - uses: actions/checkout@v4 # Forgejo-compatible (node20) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Delete branch run: | diff --git a/.github/workflows/pr_label.yml b/.github/workflows/pr_label.yml old mode 100755 new mode 100644 index 7d0d65970..5e3ba18b2 --- a/.github/workflows/pr_label.yml +++ b/.github/workflows/pr_label.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check PR and Add Label - uses: actions/github-script@v7 # Forgejo-compatible (node20) + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const prAuthor = context.payload.pull_request.user.login; diff --git a/.github/workflows/update_website.yml b/.github/workflows/update_website.yml old mode 100755 new mode 100644 index 600ed1b06..69171e9d8 --- a/.github/workflows/update_website.yml +++ b/.github/workflows/update_website.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Update website on release - uses: benc-uk/workflow-dispatch@v1.2.2 # Forgejo-compatible (node16) + uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 with: workflow: Deploy repo: komikku-app/komikku-app.github.io diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..197429c5f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,218 @@ +# Komikku – AI Agent Guide + +Komikku is an Android manga reader (min SDK 26, target SDK 36, JVM 17 / Kotlin) forked from **Mihon** + **TachiyomiSY**. Stack: Jetpack Compose + Material3, Voyager navigation, SQLDelight, Injekt DI. `applicationId`: `app.komikku`. + +--- + +## Mandatory rules for AI agents + +**Read this section before every change.** These rules override shortcuts (e.g. copying nearby `MR` imports or only running `compileDebugKotlin`). + +### Git + +| Rule | Required behavior | +|------|-------------------| +| Branch | Create a **feature branch** for the task (`git checkout -b /`). | +| Commit | **OK** on a feature branch when work is ready. **Never** commit directly to `master` / `main` unless the user explicitly asks. | +| Push | **OK** to push the **current feature branch** when work is ready. **Never** push to `master` / `main` unless the user explicitly asks. | + +Before `git push`, confirm the current branch is not `master` or `main` (`git branch --show-current`). + +### Internationalization (strings) + +| String kind | Module | Resource class | Base folder only | +|-------------|--------|----------------|------------------| +| Komikku-only (new features, KMK UI, library-update errors, WebDAV, Discord, etc.) | `i18n-kmk/` | **`KMR`** | `i18n-kmk/src/commonMain/moko-resources/base/` | +| Shared Mihon / upstream behavior | `i18n/` | **`MR`** | `i18n/src/commonMain/moko-resources/base/` | +| TachiyomiSY-only | `i18n-sy/` | **`SYMR`** | `i18n-sy/src/commonMain/moko-resources/base/` | + +**Hard rules:** + +- **Never** add Komikku-specific strings to `i18n/` or `i18n-sy/`. +- **Never** edit non-`base` locale `strings.xml` or `plurals.xml` files in `i18n-kmk/`, `i18n/`, or `i18n-sy/` (Weblate owns translations). +- Import: `import tachiyomi.i18n.kmk.KMR` for Komikku strings. +- If a change is inside `// KMK -->` … `// KMK <--` or adds Komikku-only behavior, default to **`KMR` + `i18n-kmk`**. + +**Self-check before finishing:** `git diff` must not add new `` or `` entries under non-`base` locales in `i18n-kmk/src/`, `i18n/src/`, or `i18n-sy/src/`. + +### Formatting & build verification + +**“Build passes” is not enough.** After Kotlin/XML edits, run **in this order** before marking work complete: + +```bash +./gradlew spotlessApply # fix formatting +./gradlew spotlessCheck # must pass (same as CI) +./gradlew assembleDebug # or :app:compileDebugKotlin for a faster compile-only check +``` + +- **Do not** skip `spotlessCheck` when verifying changes. +- If `spotlessCheck` fails, run `spotlessApply` and re-run `spotlessCheck`. +- On Cloud VM, export `ANDROID_HOME` and `JAVA_HOME` first (see [Cursor Cloud](#cursor-cloud-specific-instructions)). + +--- + +## Module layout + +| Module | Purpose | +|--------|---------| +| `app/` | UI (`eu.kanade.*`, `exh/`, `mihon/`), DI, workers, build variants | +| `domain/` | Use cases in `…/interactor/` (e.g. `GetManga`), models, repo interfaces | +| `data/` | SQLDelight DB, `*RepositoryImpl` (`tachiyomi.data.*`) | +| `core:common/` | Network (OkHttp), security, storage, shared utils | +| `core:archive/` | CBZ/archive reading with optional encryption | +| `core-metadata/` | Comic-info metadata parsing | +| `source-api/` / `source-local/` | Extension `Source` API + local source | +| `presentation-core/` | Shared Compose components | +| `presentation-widget/` | Home-screen Glance widget | +| `i18n/` | Mihon strings → `MR` (moko-resources) | +| `i18n-kmk/` | Komikku strings → `KMR` | +| `i18n-sy/` | TachiyomiSY strings → `SYMR` | +| `flagkit/` | Country-flag drawables | +| `telemetry/` | Firebase/Crashlytics (noop unless `-Pinclude-telemetry`) | +| `macrobenchmark/` | Macrobenchmark tests | + +Dependency flow: `app` → `domain` → `source-api`; `data` implements `domain` repos. + +Version catalogs: `gradle/libs.versions.toml`, `kotlinx.versions.toml`, `androidx.versions.toml`, `compose.versions.toml`, `sy.versions.toml`. + +--- + +## Architecture + +**DI** – `uy.kohesive.injekt` (not Hilt). Register in `AppModule.kt`, `DomainModule.kt`, `KMKDomainModule.kt`, `SYDomainModule.kt` via `addSingleton` / `addSingletonFactory`. Resolve with `Injekt.get()` or `injectLazy()`. + +**UI & navigation** – [Voyager](https://voyager.adriel.cafe/): `Screen` in `eu.kanade.tachiyomi.ui.*`, composables in `eu.kanade.presentation.*`. Base type: `eu.kanade.presentation.util.Screen`. State via `rememberScreenModel { … }`; most models extend `StateScreenModel` or bases like `SearchScreenModel`; some use plain `ScreenModel`. Prefer `screenModelScope` and `ioCoroutineScope`; use `launchIO` / `withIOContext` from `tachiyomi.core.common.util.lang`. `rememberCoroutineScope()` is fine in Compose; long-lived services may use their own `CoroutineScope`. + +**Activities (not Voyager)** – `MainActivity` (shell), `ReaderActivity` + `ReaderViewModel`, `WebViewActivity`, `UnlockActivity`, OAuth login activities, `DeepLinkActivity`. Reader: `ReaderActivity.newIntent(context, mangaId, chapterId)`. Web: both `WebViewScreen` (Voyager) and `WebViewActivity.newIntent(...)`. + +Example: `DeepLinkScreen` + `DeepLinkScreenModel` in `app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/`. + +**Domain / data** – One class per operation under `domain/…/interactor/` (verb names, not `*Interactor` suffix). Also `app/src/main/java/eu/kanade/domain/…/interactor/` for app-specific cases. Wire repos in `eu.kanade.domain.DomainModule.kt` (+ `KMKDomainModule`, `SYDomainModule`). + +**Database** – SQLDelight in `data/src/main/sqldelight/tachiyomi/` (`.sq` queries, `migrations/*.sqm`). After schema changes add a new `.sqm` and often `// KMK` blocks in `.sq` / mappers. Regenerate: `./gradlew :data:generateSqlDelightInterface` (or any compile that touches `:data`). + +**App preference migrations** – `app/src/main/java/mihon/core/migration/migrations/` (`mihon.core.migration.Migration`). + +**Images** – Coil 3 (`coil3.*`, `context.imageLoader`). No Glide/Picasso. + +--- + +## Komikku-specific work + +- **Strings:** see [Mandatory rules – Internationalization](#mandatory-rules-for-ai-agents). Summary: Komikku → **`KMR`** / `i18n-kmk/…/base/` only. +- Do not edit locale `strings.xml` in `i18n/` or `i18n-sy/` except when syncing upstream; translations via [Weblate](https://hosted.weblate.org/engage/komikku-app/). +- Komikku code/DI: search `// KMK` (e.g. `KMKDomainModule`, `HideCategory`, library-update errors). +- Prefs: `eu.kanade.domain.*.service.*Preferences` (e.g. `SourcePreferences.relatedMangas()`). + +**Examples (Komikku → `i18n-kmk`, not `i18n`):** library update error UI, sync-before-update messages, WebDAV/Discord settings, updater notifications, `mihon/feature/*` Komikku screens. + +--- + +## Extensions & sources + +- Catalog sources: installable APK extensions (not in this repo). +- In-repo: delegated sources and metadata in `exh/` (E-Hentai, NHentai, MangaDex, `exh/recs/`). +- `source-api`: `eu.kanade.tachiyomi.source.*` — avoid breaking extension ABI. + +--- + +## Build & CI + +Build types: `debug` (`.dev`), `release`, `releaseTest` (`.rt`), `foss` (`.foss`), `preview` (`.beta`, CI default), `benchmark`. + +Gradle `-P` flags (`buildSrc/.../BuildConfig.kt`): + +| Flag | Effect | +|------|--------| +| `include-telemetry` | Firebase Analytics + Crashlytics | +| `enable-updater` | In-app update checker | +| `disable-code-shrink` | Skip R8 minification | +| `include-dependency-info` | Dependency metadata in APK | + +```bash +./gradlew spotlessApply # format (run before spotlessCheck) +./gradlew spotlessCheck # REQUIRED before considering work done (CI gate) +./gradlew assemblePreview # main CI/dev APK +./gradlew assemblePreview -Pinclude-telemetry -Penable-updater # full upstream CI build +./gradlew testReleaseUnitTest # CI unit tests (or ./gradlew test for all modules) +./gradlew installDebug # device install +./gradlew :data:generateSqlDelightInterface # after .sq / .sqm changes +``` + +**Agent verification checklist (minimum):** `spotlessApply` → `spotlessCheck` → `assembleDebug` (or `compileDebugKotlin` only if the user asked for a quick compile check—but still run Spotless). + +JDK **17**. + +--- + +## Fork-origin markers + +Preserve inline blocks when editing: + +```kotlin +// KMK --> … // KMK <-- Komikku +// SY --> … // SY <-- TachiyomiSY +// EXH --> … // EXH <-- E-Hentai / exh (existing); prefer KMK for new Komikku-only code +``` + +Package roots: `eu.kanade.tachiyomi.*` (legacy UI), `tachiyomi.*` (domain/data), `mihon.*` (Mihon upstream), `exh.*` (enhanced sources). + +--- + +## Tests + +- Unit tests: `domain/src/test/`; app: `app/src/test/.../MigratorTest.kt`. No broad UI test suite. + +--- + +## Conventions + +- **Logging** – Prefer `xLogE()` / `xLog()` helpers from `exh.log` for Komikku code, Mihon uses `logcat { }` from `tachiyomi.core.common.util.system`. Avoid raw `android.util.Log`. +- **Formatting** – Spotless + ktlint (`buildSrc/.../mihon.code.lint.gradle.kts`). Agents **must** run `spotlessApply` and `spotlessCheck` (see [Mandatory rules](#mandatory-rules-for-ai-agents)). +- **Fork edits** – New Komikku features inside `// KMK` islands; keep `// SY` / `// EXH` blocks intact when merging upstream. + +--- + +## Key files + +- `App.kt` – Injekt bootstrap, logging setup +- `MainActivity.kt` – Voyager host +- `app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt` – core DI +- `app/src/main/java/eu/kanade/domain/DomainModule.kt` – domain interactors +- `buildSrc/.../BuildConfig.kt`, `AndroidConfig.kt` – flags, SDK versions +- `app/build.gradle.kts`, `settings.gradle.kts` + +--- + +## Cursor Cloud specific instructions + +### Environment + +The VM update script installs the Android SDK (platform 36, build-tools 35.0.1, platform-tools, cmdline-tools) into `/opt/android-sdk` and writes `local.properties` with `sdk.dir`. JDK 21 is pre-installed and works fine for compiling to JVM target 17. `ANDROID_HOME`, `JAVA_HOME`, and `PATH` are set in `~/.bashrc`. + +### Running key commands + +All Gradle commands require the environment variables set above. Export them before invoking `./gradlew` if running in a fresh shell: + +```bash +export ANDROID_HOME=/opt/android-sdk +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +``` + +| Task | Command | +|------|---------| +| **Required format fix** | `./gradlew spotlessApply` (run first after code edits) | +| **Required format gate** | `./gradlew spotlessCheck` (must pass before task is done) | +| Debug APK build | `./gradlew assembleDebug` | +| Preview APK build (CI) | `./gradlew assemblePreview` | +| Unit tests (CI) | `./gradlew testReleaseUnitTest` | +| All module tests | `./gradlew test` | +| SQLDelight codegen | `./gradlew :data:generateSqlDelightInterface` | + +### Gotchas + +- First Gradle build downloads ~1 GB of dependencies; subsequent builds use the Gradle cache and are much faster. +- `local.properties` is `.gitignore`d — it must be recreated if missing (the update script handles this). +- No Android emulator or device is available on the Cloud VM, so `installDebug` will fail. Build verification is done via `assembleDebug`. +- `google-services.json` and `client_secrets.json` are not present (CI secrets); builds without `-Pinclude-telemetry` succeed without them. +- Gradle daemon may use significant memory (`-Xmx4g` in `gradle.properties`). If OOM occurs, kill and restart the daemon with `./gradlew --stop`. diff --git a/CHANGELOG.md b/CHANGELOG.md index d9139dd71..4c5abddca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,54 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] ### Added +- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) + +### Changed +- Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) +- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383)) + +### Fixed +- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184)) + +## [v0.19.9] - 2026-04-11 +### Fixed +- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich)) + +## [v0.19.8] - 2026-04-11 +### Added +- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200)) +- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203)) + +### Improved +- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155)) + +## [v0.19.7] - 2026-03-23 +Same as v0.19.6 + +## [v0.19.6] - 2026-03-23 +### Fixed +- Fix app crashing when trying to add extension repo with existing signature ([@AntsyLich](https://github.com/AntsyLich)) ([`cb0e329`](https://github.com/mihonapp/mihon/commit/cb0e329)) +- Potentially fix 'database is locked' crash ([@AntsyLich](https://github.com/AntsyLich)) ([`f8e82b9`](https://github.com/mihonapp/mihon/commit/f8e82b9)) +- Fix occasional crash when mass installing/uninstalling extension using `PackageManager` ([@AntsyLich](https://github.com/AntsyLich)) ([`42daa3f`](https://github.com/mihonapp/mihon/commit/42daa3f)) +- Fix app crash on startup on some Android TV ([@AntsyLich](https://github.com/AntsyLich)) ([`b40f1d3`](https://github.com/mihonapp/mihon/commit/b40f1d3)) + +## [v0.19.5] - 2026-03-20 +### Changed +- Retry in reader now redownloads image ([@AntsyLich](https://github.com/AntsyLich)) ([#3089](https://github.com/mihonapp/mihon/pull/3089)) + +### Fixed +- Fix performance regression introduced in v0.19.4 ([@AntsyLich](https://github.com/AntsyLich)) ([#3082](https://github.com/mihonapp/mihon/pull/3082)) +- Fix duplicate key crash in duplicate detection ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3040](https://github.com/mihonapp/mihon/pull/3040)) +- Fix MangaUpdates HTTP 4XX errors ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3021](https://github.com/mihonapp/mihon/pull/3021)) +- Fix WebView JavaScript dialogs popup after screen is closed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3041](https://github.com/mihonapp/mihon/pull/3041)) +- Fix extension actions disappearing after installing and uninstalling in same session ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3049](https://github.com/mihonapp/mihon/pull/3049)) + +## [v0.19.4] - 2026-02-25 +### Added - Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673)) - Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833)) - Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851)) +- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891)) - Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927)) - Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928)) @@ -22,10 +67,16 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266)) - Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832)) - Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874)) +- Rework internals for better performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2955](https://github.com/mihonapp/mihon/pull/2955)) +- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977)) +- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2978)) ### Changed - Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) - Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675)) +- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995)) +- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906)) +- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773)) ### Fixed - Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680)) @@ -34,6 +85,10 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784)) - Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779)) - Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837)) +- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816)) +- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2993)) +- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2994)) +- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000)) ### Other - Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836)) diff --git a/README.md b/README.md index 9551af633..3cbd25785 100755 --- a/README.md +++ b/README.md @@ -126,7 +126,6 @@ Custom sources: Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source: * 8Muses (EroMuse) -* HBrowse * Mangadex * NHentai * Puruin diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 17d45026e..cb25dd904 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,7 +28,7 @@ android { defaultConfig { applicationId = "app.komikku" - versionCode = 78 + versionCode = 79 versionName = "1.13.6" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6b8216079..f8dfadd5b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -307,3 +307,5 @@ -dontwarn org.ietf.jgss.GSSManager -dontwarn org.ietf.jgss.GSSName -dontwarn org.ietf.jgss.Oid +-dontwarn com.google.re2j.Matcher +-dontwarn com.google.re2j.Pattern diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9adfec8ee..3b303755c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,8 +73,8 @@ - - + + @@ -84,6 +84,17 @@ + + + + + + + + + + + @@ -241,7 +252,7 @@ + android:foregroundServiceType="dataSync" /> - - - - - - - - - - - - - - - - @@ -363,21 +358,6 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/baseline-prof.txt b/app/src/main/baseline-prof.txt index d6333726b..af3c28df3 100644 --- a/app/src/main/baseline-prof.txt +++ b/app/src/main/baseline-prof.txt @@ -16701,9 +16701,7 @@ HSPLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLa PLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLazy$9;->()V Leu/kanade/tachiyomi/source/online/all/NHentai; Leu/kanade/tachiyomi/source/online/english/EightMuses; -Leu/kanade/tachiyomi/source/online/english/HBrowse; Leu/kanade/tachiyomi/source/online/english/Pururin; -Leu/kanade/tachiyomi/source/online/english/Tsumino; Leu/kanade/tachiyomi/ui/base/activity/BaseActivity; HSPLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;->()V PLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;->()V diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 85a7ec19d..cd12c4945 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -25,17 +25,16 @@ import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.TrackChapter -import mihon.data.repository.ExtensionRepoRepositoryImpl +import mihon.data.extension.repository.ExtensionStoreRepositoryImpl +import mihon.data.extension.service.ExtensionStoreService import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount -import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.data.category.CategoryRepositoryImpl @@ -59,6 +58,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetSortModeForCategory import tachiyomi.domain.category.interactor.UpdateCategory import tachiyomi.domain.category.repository.CategoryRepository +import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -165,6 +165,7 @@ class DomainModule : InjektModule { addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } addFactory { GetChaptersByMangaId(get()) } + addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) } addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { SetReadStatus(get(), get(), get(), get(), get()) } @@ -205,14 +206,14 @@ class DomainModule : InjektModule { addFactory { ToggleSourcePin(get()) } addFactory { TrustExtension(get(), get()) } - addSingletonFactory { ExtensionRepoRepositoryImpl(get()) } - addFactory { ExtensionRepoService(get(), get()) } - addFactory { GetExtensionRepo(get()) } - addFactory { GetExtensionRepoCount(get()) } - addFactory { CreateExtensionRepo(get(), get()) } - addFactory { DeleteExtensionRepo(get()) } - addFactory { ReplaceExtensionRepo(get()) } - addFactory { UpdateExtensionRepo(get(), get()) } + addSingletonFactory { ExtensionStoreService(get(), get(), get()) } + addSingletonFactory { ExtensionStoreRepositoryImpl(get(), get()) } + addFactory { AddExtensionStore(get()) } + addFactory { GetExtensionStoreCountAsFlow(get()) } + addFactory { GetExtensionStores(get()) } + addFactory { RemoveExtensionStore(get()) } + addFactory { UpdateExtensionStores(get()) } + addFactory { ToggleIncognito(get()) } addFactory { GetIncognitoState(get(), get(), get()) } } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt index dc7d2c304..6b25dc37b 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt @@ -28,11 +28,7 @@ class GetExtensionLanguages( ) { enabledLanguage, availableExtensions -> availableExtensions .flatMap { ext -> - if (ext.sources.isEmpty()) { - listOf(ext.lang) - } else { - ext.sources.map { it.lang } - } + ext.sources.map { it.lang } } .distinct() .sortedWith( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt index c7825a26a..b78b9af41 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt @@ -50,9 +50,6 @@ class GetExtensionsByType( (showNsfwSources || !extension.isNsfw) } .flatMap { ext -> - if (ext.sources.isEmpty()) { - return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() - } ext.sources.filter { it.lang in enabledLanguages } .map { ext.copy( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt index f70984952..7e8c12564 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt @@ -4,21 +4,21 @@ import android.content.pm.PackageInfo import androidx.core.content.pm.PackageInfoCompat import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.util.system.isDebugBuildType -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.getAndSet class TrustExtension( - private val extensionRepoRepository: ExtensionRepoRepository, + private val repository: ExtensionStoreRepository, private val preferences: SourcePreferences, ) { suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List): Boolean { // KMK --> if (isDebugBuildType) return true - if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true + if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true // KMK <-- - val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() + val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet() val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get() } diff --git a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index 463dff7d3..7d0fbf1f0 100644 --- a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -10,8 +10,8 @@ import java.util.UUID class SyncPreferences( private val preferenceStore: PreferenceStore, ) { - fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org") - fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "") + fun clientHost() = preferenceStore.getString("connection_sync_client_host", "https://sync.tachiyomi.org") + fun clientAPIKey() = preferenceStore.getString("connection_sync_client_api_key", "") fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L) fun lastSyncEtag() = preferenceStore.getString("sync_etag", "") @@ -20,19 +20,19 @@ class SyncPreferences( fun syncService() = preferenceStore.getInt("sync_service", 0) // KMK --> - fun webDavUrl() = preferenceStore.getString("webdav_url", "") - fun webDavUsername() = preferenceStore.getString("webdav_username", "") - fun webDavPassword() = preferenceStore.getString("webdav_password", "") - fun webDavFolder() = preferenceStore.getString("webdav_folder", "komikku") + fun webDavUrl() = preferenceStore.getString("connection_webdav_url", "") + fun webDavUsername() = preferenceStore.getString("connection_webdav_username", "") + fun webDavPassword() = preferenceStore.getString("connection_webdav_password", "") + fun webDavFolder() = preferenceStore.getString("connection_webdav_folder", "komikku") // KMK <-- fun googleDriveAccessToken() = preferenceStore.getString( - Preference.appStateKey("google_drive_access_token"), + Preference.appStateKey("connection_google_drive_access_token"), "", ) fun googleDriveRefreshToken() = preferenceStore.getString( - Preference.appStateKey("google_drive_refresh_token"), + Preference.appStateKey("connection_google_drive_refresh_token"), "", ) @@ -55,39 +55,39 @@ class SyncPreferences( fun getSyncSettings(): SyncSettings { return SyncSettings( - libraryEntries = preferenceStore.getBoolean("library_entries", true).get(), - categories = preferenceStore.getBoolean("categories", true).get(), - chapters = preferenceStore.getBoolean("chapters", true).get(), - tracking = preferenceStore.getBoolean("tracking", true).get(), - history = preferenceStore.getBoolean("history", true).get(), - appSettings = preferenceStore.getBoolean("appSettings", true).get(), - extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(), - sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(), - privateSettings = preferenceStore.getBoolean("privateSettings", true).get(), + libraryEntries = preferenceStore.getBoolean("sync_library_entries", true).get(), + categories = preferenceStore.getBoolean("sync_categories", true).get(), + chapters = preferenceStore.getBoolean("sync_chapters", true).get(), + tracking = preferenceStore.getBoolean("sync_tracking", true).get(), + history = preferenceStore.getBoolean("sync_history", true).get(), + appSettings = preferenceStore.getBoolean("sync_appSettings", true).get(), + extensionStores = preferenceStore.getBoolean("sync_extensionStores", true).get(), + sourceSettings = preferenceStore.getBoolean("sync_sourceSettings", true).get(), + privateSettings = preferenceStore.getBoolean("sync_privateSettings", true).get(), // SY --> - customInfo = preferenceStore.getBoolean("customInfo", true).get(), - readEntries = preferenceStore.getBoolean("readEntries", true).get(), - savedSearchesFeeds = preferenceStore.getBoolean("savedSearchesFeeds", true).get(), + customInfo = preferenceStore.getBoolean("sync_customInfo", true).get(), + readEntries = preferenceStore.getBoolean("sync_readEntries", true).get(), + savedSearchesFeeds = preferenceStore.getBoolean("sync_savedSearchesFeeds", true).get(), // SY <-- ) } fun setSyncSettings(syncSettings: SyncSettings) { - preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries) - preferenceStore.getBoolean("categories", true).set(syncSettings.categories) - preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters) - preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking) - preferenceStore.getBoolean("history", true).set(syncSettings.history) - preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings) - preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings) - preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings) - preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings) + preferenceStore.getBoolean("sync_library_entries", true).set(syncSettings.libraryEntries) + preferenceStore.getBoolean("sync_categories", true).set(syncSettings.categories) + preferenceStore.getBoolean("sync_chapters", true).set(syncSettings.chapters) + preferenceStore.getBoolean("sync_tracking", true).set(syncSettings.tracking) + preferenceStore.getBoolean("sync_history", true).set(syncSettings.history) + preferenceStore.getBoolean("sync_appSettings", true).set(syncSettings.appSettings) + preferenceStore.getBoolean("sync_extensionStores", true).set(syncSettings.extensionStores) + preferenceStore.getBoolean("sync_sourceSettings", true).set(syncSettings.sourceSettings) + preferenceStore.getBoolean("sync_privateSettings", true).set(syncSettings.privateSettings) // SY --> - preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo) - preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries) - preferenceStore.getBoolean("savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds) + preferenceStore.getBoolean("sync_customInfo", true).set(syncSettings.customInfo) + preferenceStore.getBoolean("sync_readEntries", true).set(syncSettings.readEntries) + preferenceStore.getBoolean("sync_savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds) // SY <-- } diff --git a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt index 2563bcbc9..55c69a0e6 100644 --- a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt +++ b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt @@ -7,7 +7,7 @@ data class SyncSettings( val tracking: Boolean = true, val history: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 2f6bc8865..55ff3fb23 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -86,12 +86,12 @@ fun ExtensionDetailsScreen( val uriHandler = LocalUriHandler.current val url = remember(state.extension) { val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() - regex.find(state.extension?.repoUrl.orEmpty()) + regex.find(state.extension?.store?.indexUrl.orEmpty()) ?.let { val (user, repo) = it.destructured "https://github.com/$user/$repo" } - ?: state.extension?.repoUrl + ?: state.extension?.store?.indexUrl } Scaffold( @@ -270,14 +270,17 @@ private fun DetailsHeader( if (extension is Extension.Installed) { append("\n\n") - append( + appendLine( """ Update available: ${extension.hasUpdate} - Obsolete: ${extension.isObsolete} + Orphaned: ${extension.isObsolete} Shared: ${extension.isShared} - Repository: ${extension.repoUrl} """.trimIndent(), ) + val store = extension.store + if (store != null) { + append("Repository: ${store.indexUrl}") + } } } context.copyToClipboard("Extension Debug information", extDebugInfo) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index d287d5ac7..7e6ac8a10 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -48,7 +48,7 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.extension.model.Extension @@ -58,6 +58,8 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.sy.SYMR @@ -110,9 +112,9 @@ fun ExtensionScreen( modifier = Modifier.padding(contentPadding), actions = persistentListOf( EmptyScreenAction( - stringRes = MR.strings.label_extension_repos, + stringRes = MR.strings.extensionStores, icon = Icons.Outlined.Settings, - onClick = { navigator.push(ExtensionReposScreen()) }, + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), ) @@ -195,9 +197,9 @@ private fun ExtensionContent( // KMK --> KMR.strings.extensions_page_more -> { { - Button(onClick = { navigator?.push(ExtensionReposScreen()) }) { + Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) { Text( - text = stringResource(MR.strings.action_add_repo), + text = stringResource(MR.strings.action_addExtensionStore), style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.onPrimary, ), @@ -401,7 +403,7 @@ private fun ExtensionItemContent( } // KMK --> - Text(text = extension.repoName?.let { "@$it" } ?: "(?)") + Text(text = extension.storeName?.let { "@$it" } ?: "(?)") // KMK <-- val warning = when { @@ -604,11 +606,11 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - apkName = "Test", + apkUrl = "Test", iconUrl = "", - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), ) val extInstalled = Extension.Installed( name = "Tachiyomi", @@ -619,9 +621,9 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), pkgFactory = null, icon = null, hasUpdate = false, @@ -638,12 +640,12 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", ) Column { ExtensionItemContent( extension = extAvail.copy( - repoName = "Repository extensions minion multiple languages various sources", + storeName = "Repository extensions minion multiple languages various sources", ), installStep = InstallStep.Idle, ) diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 5a5dd79ae..50ab99dbe 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -336,47 +336,46 @@ fun SourceFeedToolbar( // KMK --> actions = { AppBarActions( - actions = persistentListOf().builder() - .apply { - add(bulkSelectionButton(isRunning, toggleSelectionMode)) + actions = persistentListOf().builder().apply { + add(bulkSelectionButton(isRunning, toggleSelectionMode)) - onWebViewClick?.let { - add( - AppBar.Action( - title = stringResource(MR.strings.action_web_view), - onClick = { onWebViewClick() }, - icon = Icons.Outlined.Public, - ), - ) - } - - // KMK --> + onWebViewClick?.let { func -> add( - AppBar.OverflowAction( - title = stringResource(MR.strings.pref_incognito_mode), - onClick = onToggleIncognito, + AppBar.Action( + title = stringResource(MR.strings.action_web_view), + onClick = { func() }, + icon = Icons.Outlined.Public, ), ) - // KMK <-- - - onSortFeedClick?.let { - add( - AppBar.OverflowAction( - title = stringResource(KMR.strings.action_sort_feed), - onClick = { onSortFeedClick() }, - ), - ) - } - - onSourceSettingClick?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.label_settings), - onClick = { onSourceSettingClick() }, - ), - ) - } } + + // KMK --> + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.pref_incognito_mode), + onClick = onToggleIncognito, + ), + ) + // KMK <-- + + onSortFeedClick?.let { func -> + add( + AppBar.OverflowAction( + title = stringResource(KMR.strings.action_sort_feed), + onClick = { func() }, + ), + ) + } + + onSourceSettingClick?.let { func -> + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.label_settings), + onClick = { func() }, + ), + ) + } + } .build(), ) }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt index 2eb0a609d..cc2db3449 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt @@ -209,7 +209,7 @@ fun BrowseSourceEHentaiListItem( MangaCoverHide.Book( modifier = Modifier .fillMaxHeight(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -219,7 +219,7 @@ fun BrowseSourceEHentaiListItem( .fillMaxHeight(), // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, // KMK <-- data = coverData, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt index 0d6cbf5c2..6a562b74a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt @@ -39,27 +39,26 @@ fun BrowseSourceSimpleToolbar( var selectingDisplayMode by remember { mutableStateOf(false) } // KMK --> AppBarActions( - actions = persistentListOf().builder() - .apply { - displayMode?.let { - add( - AppBar.Action( - title = stringResource(MR.strings.action_display_mode), - icon = if (displayMode == LibraryDisplayMode.List) { - Icons.AutoMirrored.Filled.ViewList - } else { - Icons.Filled.ViewModule - }, - onClick = { selectingDisplayMode = true }, - ), - ) - } - toggleSelectionMode?.let { - add( - bulkSelectionButton(isRunning, toggleSelectionMode), - ) - } + actions = persistentListOf().builder().apply { + displayMode?.let { mode -> + add( + AppBar.Action( + title = stringResource(MR.strings.action_display_mode), + icon = if (mode == LibraryDisplayMode.List) { + Icons.AutoMirrored.Filled.ViewList + } else { + Icons.Filled.ViewModule + }, + onClick = { selectingDisplayMode = true }, + ), + ) } + toggleSelectionMode?.let { mode -> + add( + bulkSelectionButton(isRunning, mode), + ) + } + } .build(), ) // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt b/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt index c6689ebda..653b0f2a1 100644 --- a/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/BulkSelectionToolbar.kt @@ -30,53 +30,51 @@ fun BulkSelectionToolbar( titleContent = { Text(text = "$selectedCount") }, actions = { AppBarActions( - actions = persistentListOf() - .builder() - .apply { - if (onSelectAll != null) { - add( - AppBar.Action( - title = stringResource(MR.strings.action_select_all), - icon = Icons.Filled.SelectAll, - onClick = onSelectAll, - ), - ) - } - if (onReverseSelection != null) { - add( - AppBar.Action( - title = stringResource(MR.strings.action_select_inverse), - icon = Icons.Outlined.FlipToBack, - onClick = onReverseSelection, - ), - ) - } - if (isRunning) { - add( - AppBar.ActionCompose( - title = stringResource(KMR.strings.action_bulk_select), - ) { - CircularProgressIndicator( - modifier = Modifier - .size(24.dp), - strokeWidth = 2.dp, - ) + actions = persistentListOf().builder().apply { + if (onSelectAll != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Filled.SelectAll, + onClick = onSelectAll, + ), + ) + } + if (onReverseSelection != null) { + add( + AppBar.Action( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onReverseSelection, + ), + ) + } + if (isRunning) { + add( + AppBar.ActionCompose( + title = stringResource(KMR.strings.action_bulk_select), + ) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + strokeWidth = 2.dp, + ) + }, + ) + } else { + add( + AppBar.Action( + title = stringResource(MR.strings.add_to_library), + icon = Icons.Filled.Favorite, + onClick = { + if (selectedCount > 0) { + onChangeCategoryClick() + } }, - ) - } else { - add( - AppBar.Action( - title = stringResource(MR.strings.add_to_library), - icon = Icons.Filled.Favorite, - onClick = { - if (selectedCount > 0) { - onChangeCategoryClick() - } - }, - ), - ) - } - }.build(), + ), + ) + } + }.build(), ) }, isActionMode = true, diff --git a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt index 7bdf97d71..dfe7998b7 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt @@ -44,6 +44,7 @@ private fun DownloadDropdownMenuItems( DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10), DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25), DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread), + DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked), ) options.forEach { (downloadAction, string) -> diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt index 309fa2d1a..5485474ec 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonMangaItem.kt @@ -105,7 +105,7 @@ fun MangaCompactGridItem( MangaCoverHide.Book( modifier = Modifier .fillMaxWidth(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -119,7 +119,7 @@ fun MangaCompactGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, // KMK <-- ) @@ -241,7 +241,7 @@ fun MangaComfortableGridItem( MangaCoverHide.Book( modifier = Modifier .fillMaxWidth(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -255,7 +255,7 @@ fun MangaComfortableGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, onCoverLoaded = { _, result -> val image = result.result.image @@ -274,7 +274,7 @@ fun MangaComfortableGridItem( data = coverData, // KMK --> alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, onCoverLoaded = { _, result -> val image = result.result.image @@ -461,7 +461,7 @@ fun MangaListItem( MangaCoverHide.Square( modifier = Modifier .fillMaxHeight(), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, ) } else { @@ -475,7 +475,7 @@ fun MangaListItem( data = coverData, // KMK --> alpha = coverAlpha, - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected }, tint = onBgColor, size = MangaCover.Size.Big, // KMK <-- diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt index db940cafd..034d2387c 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -67,7 +67,7 @@ fun LibraryUpdateErrorScreen( onInvertSelection: () -> Unit, onErrorsDelete: () -> Unit, onErrorDelete: (Long) -> Unit, - onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, + onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit, navigateUp: () -> Unit, ) { BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt index 1d1f2d13e..a18baecd4 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/components/LibraryUpdateErrorUiItem.kt @@ -44,7 +44,7 @@ import tachiyomi.presentation.core.util.selectedBackground internal fun LazyListScope.libraryUpdateErrorUiItems( uiModels: List, selectionMode: Boolean, - onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, + onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit, onClick: (LibraryUpdateErrorItem) -> Unit, onClickCover: (LibraryUpdateErrorItem) -> Unit, onDelete: (Long) -> Unit, @@ -81,7 +81,6 @@ internal fun LazyListScope.libraryUpdateErrorUiItems( selectionMode -> onErrorSelected( libraryUpdateErrorItem, !libraryUpdateErrorItem.selected, - true, false, ) @@ -93,7 +92,6 @@ internal fun LazyListScope.libraryUpdateErrorUiItems( libraryUpdateErrorItem, !libraryUpdateErrorItem.selected, true, - true, ) }, onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode }, @@ -144,8 +142,8 @@ private fun LibraryUpdateErrorUiItem( .combinedClickable( onClick = onClick, onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, ) .padding(horizontal = MaterialTheme.padding.medium), diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index f4bacf6a2..08cdfa37e 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -94,9 +94,7 @@ import eu.kanade.tachiyomi.source.online.all.Lanraragi import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MergedMangaData @@ -108,12 +106,10 @@ import exh.source.getMainSource import exh.source.isEhBasedManga import exh.ui.metadata.adapters.EHentaiDescription import exh.ui.metadata.adapters.EightMusesDescription -import exh.ui.metadata.adapters.HBrowseDescription import exh.ui.metadata.adapters.LanraragiDescription import exh.ui.metadata.adapters.MangaDexDescription import exh.ui.metadata.adapters.NHentaiDescription import exh.ui.metadata.adapters.PururinDescription -import exh.ui.metadata.adapters.TsuminoDescription import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.service.missingChaptersCount import tachiyomi.domain.library.service.LibraryPreferences @@ -194,7 +190,7 @@ fun MangaScreen( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -404,7 +400,7 @@ private fun MangaScreenSmallImpl( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -868,7 +864,7 @@ private fun MangaScreenLargeImpl( onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, // Chapter selection - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onAllChapterSelected: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -1307,7 +1303,7 @@ private fun LazyListScope.sharedChapterItems( // SY <-- onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, - onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit, + onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit, ) { items( @@ -1376,14 +1372,14 @@ private fun LazyListScope.sharedChapterItems( chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onLongClick = { - onChapterSelected(item, !item.selected, true, true) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onChapterSelected(item, !item.selected, true) }, onClick = { onChapterItemClick( chapterItem = item, isAnyChapterSelected = isAnyChapterSelected, - onToggleSelection = { onChapterSelected(item, !item.selected, true, false) }, + onToggleSelection = { onChapterSelected(item, !item.selected, false) }, onChapterClicked = onChapterClicked, ) }, @@ -1438,15 +1434,9 @@ fun metadataDescription(source: Source): MetadataDescriptionComposable? { is EightMuses -> { state, openMetadataViewer, _ -> EightMusesDescription(state, openMetadataViewer) } - is HBrowse -> { state, openMetadataViewer, _ -> - HBrowseDescription(state, openMetadataViewer) - } is Pururin -> { state, openMetadataViewer, _ -> PururinDescription(state, openMetadataViewer) } - is Tsumino -> { state, openMetadataViewer, _ -> - TsuminoDescription(state, openMetadataViewer) - } is Lanraragi -> { state, openMetadataViewer, _ -> LanraragiDescription(state, openMetadataViewer) } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt index 8f337bb2a..c53e254b9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -6,6 +6,7 @@ enum class DownloadAction { NEXT_10_CHAPTERS, NEXT_25_CHAPTERS, UNREAD_CHAPTERS, + BOOKMARKED_CHAPTERS, } enum class EditCoverAction { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt index e56216ba0..eb134704b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt @@ -43,8 +43,8 @@ fun BaseMangaListItem( onClick = onClickItem, // KMK --> onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt index e96eb1e72..40d04db6c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt @@ -14,11 +14,11 @@ import eu.kanade.core.preference.asState import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.more.settings.Preference -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate import kotlinx.collections.immutable.persistentListOf -import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount +import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR @@ -43,9 +43,9 @@ object SettingsBrowseScreen : SearchableSettings { val navigator = LocalNavigator.currentOrThrow val sourcePreferences = remember { Injekt.get() } - val getExtensionRepoCount = remember { Injekt.get() } + val getExtensionStoreCountAsFlow = remember { Injekt.get() } - val reposCount by getExtensionRepoCount.subscribe().collectAsState(0) + val reposCount by getExtensionStoreCountAsFlow().collectAsState(0) // SY --> val scope = rememberCoroutineScope() @@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings { title = stringResource(MR.strings.pref_hide_in_library_items), ), Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.label_extension_repos), - subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount), + title = stringResource(MR.strings.extensionStores), + subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount), onClick = { - navigator.push(ExtensionReposScreen()) + navigator.push(ExtensionStoresScreen()) }, ), ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt deleted file mode 100644 index 3cd14aa2b..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt +++ /dev/null @@ -1,205 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse - -import androidx.compose.runtime.Immutable -import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import dev.icerock.moko.resources.StringResource -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.extension.ExtensionManager -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.core.common.util.lang.launchIO -import tachiyomi.i18n.MR -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionReposScreenModel( - private val getExtensionRepo: GetExtensionRepo = Injekt.get(), - private val createExtensionRepo: CreateExtensionRepo = Injekt.get(), - private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(), - private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(), - private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(), - private val extensionManager: ExtensionManager = Injekt.get(), - // KMK --> - private val sourcePreferences: SourcePreferences = Injekt.get(), - // KMK <-- -) : StateScreenModel(RepoScreenState.Loading) { - - private val _events: Channel = Channel(Int.MAX_VALUE) - val events = _events.receiveAsFlow() - - init { - screenModelScope.launchIO { - getExtensionRepo.subscribeAll() - .collectLatest { repos -> - mutableState.update { - RepoScreenState.Success( - repos = repos.toImmutableSet(), - // KMK --> - disabledRepos = sourcePreferences.disabledRepos().get(), - // KMK <-- - ) - } - } - } - - // KMK --> - sourcePreferences.disabledRepos().changes() - .onEach { disabledRepos -> - mutableState.update { - when (it) { - is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos) - else -> it - } - } - } - .launchIn(screenModelScope) - // KMK <-- - } - - /** - * Creates and adds a new repo to the database. - * - * @param baseUrl The baseUrl of the repo to create. - */ - fun createRepo(baseUrl: String) { - screenModelScope.launchIO { - when (val result = createExtensionRepo.await(baseUrl)) { - CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions() - CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl) - CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists) - is CreateExtensionRepo.Result.DuplicateFingerprint -> { - showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo)) - } - else -> {} - } - } - } - - /** - * Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found. - * - * @param newRepo The repo to insert - */ - fun replaceRepo(newRepo: ExtensionRepo) { - screenModelScope.launchIO { - replaceExtensionRepo.await(newRepo) - } - } - - /** - * Refreshes information for each repository. - */ - fun refreshRepos() { - val status = state.value - - if (status is RepoScreenState.Success) { - screenModelScope.launchIO { - updateExtensionRepo.awaitAll() - } - } - } - - /** - * Deletes the given repo from the database - */ - fun deleteRepo(baseUrl: String) { - // KMK --> - // Remove repo from disabled list - enableRepo(baseUrl) - // KMK <-- - screenModelScope.launchIO { - deleteExtensionRepo.await(baseUrl) - extensionManager.findAvailableExtensions() - } - } - - // KMK --> - fun enableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos.filterNot { it == baseUrl }.toSet(), - ) - } - } - - fun disableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl !in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos + baseUrl, - ) - } - } - - fun refreshExtensionList() { - screenModelScope.launchIO { - extensionManager.findAvailableExtensions() - } - } - // KMK <-- - - fun showDialog(dialog: RepoDialog) { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = dialog) - } - } - } - - fun dismissDialog() { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = null) - } - } - } -} - -sealed class RepoEvent { - sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent() - data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name) - data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists) -} - -sealed class RepoDialog { - data object Create : RepoDialog() - data class Delete(val repo: String) : RepoDialog() - data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog() - data class Confirm(val url: String) : RepoDialog() -} - -sealed class RepoScreenState { - - @Immutable - data object Loading : RepoScreenState() - - @Immutable - data class Success( - val repos: ImmutableSet, - val oldRepos: ImmutableSet? = null, - val dialog: RepoDialog? = null, - // KMK --> - val disabledRepos: Set = emptySet(), - // KMK <-- - ) : RepoScreenState() { - - val isEmpty: Boolean - get() = repos.isEmpty() - } -} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt similarity index 53% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt index aa0217fec..af1ce92bd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt @@ -8,20 +8,18 @@ import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConfirmDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog -import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.flow.collectLatest import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.screens.LoadingScreen -class ExtensionReposScreen( +class ExtensionStoresScreen( private val url: String? = null, ) : Screen() { @@ -30,80 +28,72 @@ class ExtensionReposScreen( val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { ExtensionReposScreenModel() } + val screenModel = rememberScreenModel { ExtensionStoresScreenModel() } val state by screenModel.state.collectAsState() LaunchedEffect(url) { - url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) } + url?.let { screenModel.addFromDeeplink(url) } } - if (state is RepoScreenState.Loading) { + if (state is ExtensionStoreScreenState.Loading) { LoadingScreen() return } - val successState = state as RepoScreenState.Success + val successState = state as ExtensionStoreScreenState.Success - ExtensionReposScreen( + ExtensionStoresScreen( state = successState, - onClickCreate = { screenModel.showDialog(RepoDialog.Create) }, - onOpenWebsite = { context.openInBrowser(it.website) }, - onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) }, + onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) }, + onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) }, + onOpenWebsite = { it.contact.website.let(context::openInBrowser) }, + onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) }, // KMK --> onClickEnable = { - screenModel.enableRepo(it) + screenModel.enableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, onClickDisable = { - screenModel.disableRepo(it) + screenModel.disableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, // KMK <-- + onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) }, onClickRefresh = { screenModel.refreshRepos() }, navigateUp = navigator::pop, ) when (val dialog = successState.dialog) { null -> {} - is RepoDialog.Create -> { - ExtensionRepoCreateDialog( + is ExtensionStoreDialog.Create -> { + ExtensionStoreCreateDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(it) }, - repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(), + storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(), + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } - is RepoDialog.Delete -> { - ExtensionRepoDeleteDialog( + is ExtensionStoreDialog.Delete -> { + ExtensionStoreDeleteDialog( onDismissRequest = screenModel::dismissDialog, - onDelete = { screenModel.deleteRepo(dialog.repo) }, - repo = dialog.repo, + onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) }, + storeName = dialog.store.name, + storeIndexUrl = dialog.store.indexUrl, ) } - is RepoDialog.Conflict -> { - ExtensionRepoConflictDialog( - onDismissRequest = screenModel::dismissDialog, - onMigrate = { screenModel.replaceRepo(dialog.newRepo) }, - oldRepo = dialog.oldRepo, - newRepo = dialog.newRepo, - ) - } - is RepoDialog.Confirm -> { - ExtensionRepoConfirmDialog( + is ExtensionStoreDialog.Confirm -> { + ExtensionStoreConfirmDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(dialog.url) }, - repo = dialog.url, + storeIndexUrl = dialog.url, + storeAlreadyExists = dialog.alreadyExists, + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } } - - LaunchedEffect(Unit) { - screenModel.events.collectLatest { event -> - if (event is RepoEvent.LocalizedMessage) { - context.toast(event.stringRes) - } - } - } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt new file mode 100644 index 000000000..f794576d3 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt @@ -0,0 +1,223 @@ +package eu.kanade.presentation.more.settings.screen.browse + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.extension.ExtensionManager +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.model.ExtensionStore +import tachiyomi.core.common.util.lang.launchIO +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresScreenModel( + private val getExtensionStores: GetExtensionStores = Injekt.get(), + private val addExtensionStore: AddExtensionStore = Injekt.get(), + private val removeExtensionStore: RemoveExtensionStore = Injekt.get(), + private val updateExtensionStores: UpdateExtensionStores = Injekt.get(), + private val extensionManager: ExtensionManager = Injekt.get(), + // KMK --> + private val sourcePreferences: SourcePreferences = Injekt.get(), + // KMK <-- +) : StateScreenModel(ExtensionStoreScreenState.Loading) { + + private inline fun updateSuccessState( + func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success, + ) { + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> it + is ExtensionStoreScreenState.Success -> func(it) + } + } + } + + init { + screenModelScope.launchIO { + getExtensionStores.subscribe() + .collectLatest { stores -> + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success( + stores = stores, + // KMK --> + disabledRepos = sourcePreferences.disabledRepos().get(), + // KMK <-- + ) + is ExtensionStoreScreenState.Success -> it.copy(stores = stores) + } + } + } + } + + // KMK --> + sourcePreferences.disabledRepos().changes() + .onEach { disabledRepos -> + mutableState.update { + when (it) { + is ExtensionStoreScreenState.Success -> it.copy(disabledRepos = disabledRepos) + else -> it + } + } + } + .launchIn(screenModelScope) + // KMK <-- + } + + /** + * Creates and adds a new repo to the database. + * + * @param indexUrl The baseUrl of the repo to create. + */ + fun createRepo(indexUrl: String) { + // KMK --> + screenModelScope.launchIO { + // KMK <-- + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true) + is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true) + else -> it.dialog + }, + ) + } + addExtensionStore(indexUrl) + .onSuccess { + extensionManager.findAvailableExtensions() + dismissDialog() + } + .onFailure { throwable -> + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + is ExtensionStoreDialog.Confirm -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + else -> it.dialog + }, + ) + } + } + } + } + + /** + * Refreshes information for each repository. + */ + fun refreshRepos() { + val status = state.value + + if (status is ExtensionStoreScreenState.Success) { + screenModelScope.launchIO { + updateExtensionStores() + } + } + } + + /** + * Deletes the given repo from the database + */ + fun deleteRepo(indexUrl: String) { + // KMK --> + // Remove repo from disabled list + enableStore(indexUrl) + // KMK <-- + screenModelScope.launchIO { + removeExtensionStore(indexUrl) + extensionManager.findAvailableExtensions() + } + } + + // KMK --> + fun enableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos.filterNot { it == indexUrl }.toSet(), + ) + } + } + + fun disableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl !in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos + indexUrl, + ) + } + } + + fun refreshExtensionList() { + screenModelScope.launchIO { + extensionManager.findAvailableExtensions() + } + } + // KMK <-- + + fun addFromDeeplink(storeIndexUrl: String) { + updateSuccessState { state -> + state.copy( + dialog = ExtensionStoreDialog.Confirm( + url = storeIndexUrl, + alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl }, + ), + ) + } + } + + fun showDialog(dialog: ExtensionStoreDialog) { + updateSuccessState { state -> + state.copy(dialog = dialog) + } + } + + fun dismissDialog() { + updateSuccessState { + it.copy(dialog = null) + } + } +} + +sealed class ExtensionStoreDialog { + data class Create(val processing: Boolean = false, val errorMessage: String? = null) : ExtensionStoreDialog() + data class Delete(val store: ExtensionStore) : ExtensionStoreDialog() + data class Confirm( + val url: String, + val alreadyExists: Boolean = false, + val processing: Boolean = false, + val errorMessage: String? = null, + ) : ExtensionStoreDialog() +} + +sealed class ExtensionStoreScreenState { + + @Immutable + data object Loading : ExtensionStoreScreenState() + + @Immutable + data class Success( + val stores: List, + val dialog: ExtensionStoreDialog? = null, + // KMK --> + val disabledRepos: Set = emptySet(), + // KMK <-- + ) : ExtensionStoreScreenState() { + + val isEmpty: Boolean + get() = stores.isEmpty() + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt deleted file mode 100644 index bd7c1cedd..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt +++ /dev/null @@ -1,188 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.input.KeyboardType -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.coroutines.delay -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.i18n.stringResource -import kotlin.time.Duration.Companion.seconds - -@Composable -fun ExtensionRepoCreateDialog( - onDismissRequest: () -> Unit, - onCreate: (String) -> Unit, - repoUrls: ImmutableSet, -) { - var name by remember { mutableStateOf("") } - - val focusRequester = remember { FocusRequester() } - val nameAlreadyExists = remember(name) { repoUrls.contains(name) } - - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - enabled = name.isNotEmpty() && !nameAlreadyExists, - onClick = { - onCreate(name) - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Column { - Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name))) - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = name, - onValueChange = { name = it }, - label = { - Text(text = stringResource(MR.strings.label_add_repo_input)) - }, - supportingText = { - val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { - MR.strings.error_repo_exists - } else { - MR.strings.information_required_plain - } - Text(text = stringResource(msgRes)) - }, - isError = name.isNotEmpty() && nameAlreadyExists, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), - singleLine = true, - ) - } - }, - ) - - LaunchedEffect(focusRequester) { - // TODO: https://issuetracker.google.com/issues/204502668 - delay(0.1.seconds) - focusRequester.requestFocus() - } -} - -@Composable -fun ExtensionRepoDeleteDialog( - onDismissRequest: () -> Unit, - onDelete: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton(onClick = { - onDelete() - onDismissRequest() - }) { - Text(text = stringResource(MR.strings.action_ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_delete_repo)) - }, - text = { - Text(text = stringResource(MR.strings.delete_repo_confirmation, repo)) - }, - ) -} - -@Composable -fun ExtensionRepoConflictDialog( - 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(MR.strings.action_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 ExtensionRepoConfirmDialog( - onDismissRequest: () -> Unit, - onCreate: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Text(text = stringResource(MR.strings.add_repo_confirmation, repo)) - }, - confirmButton = { - TextButton( - onClick = { - onCreate() - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - ) -} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt index 346e38c43..ed7ad079b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt @@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.ElevatedCard @@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.copyToClipboard -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.icons.CustomIcons +import tachiyomi.presentation.core.icons.Discord @Composable -fun ExtensionReposContent( - repos: ImmutableSet, +fun ExtensionStoresContent( + repos: List, lazyListState: LazyListState, paddingValues: PaddingValues, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, disabledRepos: Set, // KMK <-- modifier: Modifier = Modifier, @@ -65,15 +65,17 @@ fun ExtensionReposContent( ) { repos.forEach { item { - ExtensionRepoListItem( + ExtensionStoresListItem( modifier = Modifier.animateItem(), - repo = it, + store = it, onOpenWebsite = { onOpenWebsite(it) }, - onDelete = { onClickDelete(it.baseUrl) }, + onOpenDiscord = { onOpenDiscord(it) }, + onCopy = { onCopy(it) }, + onDelete = { onClickDelete(it) }, // KMK --> - onEnable = { onClickEnable(it.baseUrl) }, - onDisable = { onClickDisable(it.baseUrl) }, - isDisabled = it.baseUrl in disabledRepos, + onEnable = { onClickEnable(it) }, + onDisable = { onClickDisable(it) }, + isDisabled = it.indexUrl in disabledRepos, // KMK <-- ) } @@ -82,9 +84,11 @@ fun ExtensionReposContent( } @Composable -private fun ExtensionRepoListItem( - repo: ExtensionRepo, +private fun ExtensionStoresListItem( + store: ExtensionStore, onOpenWebsite: () -> Unit, + onOpenDiscord: () -> Unit, + onCopy: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, // KMK --> @@ -93,8 +97,6 @@ private fun ExtensionRepoListItem( onDisable: () -> Unit, // KMK <-- ) { - val context = LocalContext.current - ElevatedCard( modifier = modifier, ) { @@ -103,9 +105,9 @@ private fun ExtensionRepoListItem( modifier = Modifier .padding(start = MaterialTheme.padding.medium), ) { - val resId = repoResId(repo.signingKeyFingerprint) + val resId = repoResId(store.signingKey) Image( - bitmap = ImageBitmap.imageResource(id = resId), + painter = painterResource(id = resId), contentDescription = null, alpha = if (isDisabled) 0.4f else 1f, modifier = Modifier @@ -126,7 +128,7 @@ private fun ExtensionRepoListItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = repo.name, + text = store.name, // KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium), style = MaterialTheme.typography.titleMedium, // KMK --> @@ -142,17 +144,21 @@ private fun ExtensionRepoListItem( ) { IconButton(onClick = onOpenWebsite) { Icon( - imageVector = Icons.AutoMirrored.Outlined.OpenInNew, + imageVector = Icons.Outlined.Public, contentDescription = stringResource(MR.strings.action_open_in_browser), ) } - IconButton( - onClick = { - val url = "${repo.baseUrl}/index.min.json" - context.copyToClipboard(url, url) - }, - ) { + if (store.contact.discord != null) { + IconButton(onClick = onOpenDiscord) { + Icon( + imageVector = CustomIcons.Discord, + contentDescription = null, + ) + } + } + + IconButton(onClick = onCopy) { Icon( imageVector = Icons.Outlined.ContentCopy, contentDescription = stringResource(MR.strings.action_copy_to_clipboard), @@ -190,16 +196,18 @@ fun repoResId(signKey: String) = when (signKey) { @Preview @Composable fun ExtensionReposContentPreview() { - val repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), + val repos = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ) - ExtensionReposContent( + ExtensionStoresContent( repos = repos, lazyListState = LazyListState(), paddingValues = PaddingValues(), + onCopy = {}, onOpenWebsite = {}, + onOpenDiscord = {}, onClickDelete = {}, onClickEnable = {}, onClickDisable = {}, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt new file mode 100644 index 000000000..19436f17a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt @@ -0,0 +1,192 @@ +package eu.kanade.presentation.more.settings.screen.browse.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import kotlin.time.Duration.Companion.seconds + +@Composable +fun ExtensionStoreCreateDialog( + onDismissRequest: () -> Unit, + onCreate: (String) -> Unit, + storeIndexUrls: Set, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState() + val storeAlreadyExists by remember(storeIndexUrls) { + derivedStateOf { + val indexUrl = state.text.toString() + storeIndexUrls.contains(indexUrl) + } + } + + val focusRequester = remember { FocusRequester() } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + state = state, + label = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreInput_inputLabel)) + }, + supportingText = { + val msgRes = if (storeAlreadyExists) { + MR.strings.extensionStoresScreen_addStore_alreadyExists + } else { + MR.strings.information_required_plain + } + Text(text = errorMessage ?: stringResource(msgRes)) + }, + isError = errorMessage != null || storeAlreadyExists, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + lineLimits = TextFieldLineLimits.SingleLine, + ) + }, + confirmButton = { + TextButton( + onClick = { onCreate(state.text.toString()) }, + enabled = !processing && state.text.isNotEmpty() && !storeAlreadyExists, + ) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) + + LaunchedEffect(focusRequester) { + // TODO: https://issuetracker.google.com/issues/204502668 + delay(0.1.seconds) + focusRequester.requestFocus() + } +} + +@Composable +fun ExtensionStoreDeleteDialog( + onDismissRequest: () -> Unit, + onDelete: () -> Unit, + storeName: String, + storeIndexUrl: String, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_title)) + }, + text = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_body, storeName, storeIndexUrl)) + }, + confirmButton = { + TextButton(onClick = { + onDelete() + onDismissRequest() + }) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} + +@Composable +fun ExtensionStoreConfirmDialog( + onDismissRequest: () -> Unit, + onCreate: () -> Unit, + storeIndexUrl: String, + storeAlreadyExists: Boolean, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState(initialText = storeIndexUrl) + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreDeeplink_bodyText)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + state = state, + readOnly = true, + supportingText = when { + storeAlreadyExists -> { + { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_alreadyExists)) + } + } + errorMessage != null -> { + { + Text(text = errorMessage) + } + } + else -> null + }, + isError = errorMessage != null || storeAlreadyExists, + ) + } + }, + confirmButton = { + TextButton(onClick = onCreate, enabled = !storeAlreadyExists && !processing) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt index 651b45f51..89e8a0138 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt @@ -1,5 +1,3 @@ -@file:JvmName("ExtensionReposScreenKt") - package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.PaddingValues @@ -21,13 +19,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState import eu.kanade.tachiyomi.util.system.openInBrowser -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_HELP +import mihon.domain.extension.model.REPO_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding @@ -37,14 +35,16 @@ import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.plus @Composable -fun ExtensionReposScreen( - state: RepoScreenState.Success, +fun ExtensionStoresScreen( + state: ExtensionStoreScreenState.Success, onClickCreate: () -> Unit, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, // KMK <-- onClickRefresh: () -> Unit, navigateUp: () -> Unit, @@ -54,7 +54,7 @@ fun ExtensionReposScreen( topBar = { scrollBehavior -> AppBar( navigateUp = navigateUp, - title = stringResource(MR.strings.label_extension_repos), + title = stringResource(MR.strings.extensionStores), scrollBehavior = scrollBehavior, actions = { IconButton(onClick = onClickRefresh) { @@ -76,7 +76,7 @@ fun ExtensionReposScreen( if (state.isEmpty) { val context = LocalContext.current EmptyScreen( - MR.strings.information_empty_repos, + MR.strings.extensionStoresScreen_emptyLabel, modifier = Modifier.padding(paddingValues), // KMK --> help = { @@ -94,12 +94,14 @@ fun ExtensionReposScreen( return@Scaffold } - ExtensionReposContent( - repos = state.repos, + ExtensionStoresContent( + repos = state.stores, lazyListState = lazyListState, paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium), + onCopy = onCopy, onOpenWebsite = onOpenWebsite, + onOpenDiscord = onOpenDiscord, onClickDelete = onClickDelete, // KMK --> onClickEnable = onClickEnable, @@ -113,19 +115,21 @@ fun ExtensionReposScreen( // KMK --> @Preview @Composable -private fun ExtensionReposScreenPreview() { - val state = RepoScreenState.Success( - repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), +private fun ExtensionStoresScreenPreview() { + val state = ExtensionStoreScreenState.Success( + stores = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ), disabledRepos = setOf("https://repo"), ) - ExtensionReposScreen( + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, @@ -136,12 +140,14 @@ private fun ExtensionReposScreenPreview() { @Preview @Composable -private fun ExtensionReposScreenEmptyPreview() { - val state = RepoScreenState.Success(repos = persistentSetOf()) - ExtensionReposScreen( +private fun ExtensionStoresScreenEmptyPreview() { + val state = ExtensionStoreScreenState.Success(stores = persistentListOf()) + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt index 83e4b6c08..f198455fc 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt @@ -114,7 +114,7 @@ private class SyncSettingsSelectorModel( tracking = syncSettings.tracking, history = syncSettings.history, appSettings = syncSettings.appSettings, - extensionRepoSettings = syncSettings.extensionRepoSettings, + extensionStores = syncSettings.extensionStores, sourceSettings = syncSettings.sourceSettings, privateSettings = syncSettings.privateSettings, @@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel( tracking = backupOptions.tracking, history = backupOptions.history, appSettings = backupOptions.appSettings, - extensionRepoSettings = backupOptions.extensionRepoSettings, + extensionStores = backupOptions.extensionStores, sourceSettings = backupOptions.sourceSettings, privateSettings = backupOptions.privateSettings, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 9e8033f30..44bd792fc 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -171,7 +171,6 @@ internal fun LazyListScope.updatesUiItems( // KMK --> UpdateSelectionOptions( selected = !updatesItem.selected, - userSelected = true, fromLongPress = true, isGroup = isLeader && item.isExpandable, isExpanded = isExpanded, @@ -186,7 +185,6 @@ internal fun LazyListScope.updatesUiItems( // KMK --> UpdateSelectionOptions( selected = !updatesItem.selected, - userSelected = true, fromLongPress = false, isGroup = isLeader && item.isExpandable, isExpanded = isExpanded, @@ -288,8 +286,8 @@ private fun UpdatesUiItem( .combinedClickable( onClick = onClick, onLongClick = { - onLongClick() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() }, ) .padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp) @@ -311,7 +309,7 @@ private fun UpdatesUiItem( MangaCoverHide.Book( modifier = Modifier .width(UpdateItemWidth), - bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { selected }), + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { selected }, tint = onBgColor, size = MangaCover.Size.Medium, ) diff --git a/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt b/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt index 344a2fe6a..1eebd48bc 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/EhLoginWebViewScreen.kt @@ -80,7 +80,7 @@ fun EhLoginWebViewScreen( ) is LoadingState.Loading -> { val animatedProgress by animateFloatAsState( - (loadingState as? LoadingState.Loading)?.progress ?: 1f, + loadingState.progress, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, label = "webview_loading", ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 731f7bad4..5014f6ac5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator -import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionStoresBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator @@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference @@ -54,7 +54,7 @@ class BackupCreator( private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(), - private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(), + private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(), private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(), // KMK --> private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(), @@ -101,7 +101,7 @@ class BackupCreator( backupCategories = backupCategories(options), backupSources = backupSources(backupManga), backupPreferences = backupAppPreferences(options), - backupExtensionRepo = backupExtensionRepos(options), + backupExtensionStores = backupExtensionStores(options), backupSourcePreferences = backupSourcePreferences(options), // SY --> @@ -159,16 +159,16 @@ class BackupCreator( return sourcesBackupCreator(mangas) } - /* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List { + internal fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings) } - suspend fun backupExtensionRepos(options: BackupOptions): List { - if (!options.extensionRepoSettings) return emptyList() + internal suspend fun backupExtensionStores(options: BackupOptions): List { + if (!options.extensionStores) return emptyList() - return extensionRepoBackupCreator() + return extensionStoresBackupCreator() } fun backupSourcePreferences(options: BackupOptions): List { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 469137c35..666ba9d3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -14,7 +14,7 @@ data class BackupOptions( val history: Boolean = true, val readEntries: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, // SY --> @@ -31,7 +31,7 @@ data class BackupOptions( history, readEntries, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, privateSettings, // SY --> @@ -41,7 +41,7 @@ data class BackupOptions( ) fun canCreate() = - libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds + libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds companion object { val libraryOptions = persistentListOf( @@ -103,9 +103,9 @@ data class BackupOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = BackupOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = BackupOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -128,7 +128,7 @@ data class BackupOptions( history = array[4], readEntries = array[5], appSettings = array[6], - extensionRepoSettings = array[7], + extensionStores = array[7], sourceSettings = array[8], privateSettings = array[9], // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt deleted file mode 100644 index 2db6a6f06..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.create.creators - -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos -import eu.kanade.tachiyomi.data.backup.models.backupExtensionReposMapper -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoBackupCreator( - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), -) { - - suspend operator fun invoke(): List { - return getExtensionRepos.getAll() - .map(backupExtensionReposMapper) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt new file mode 100644 index 000000000..0ca4aa59e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import eu.kanade.tachiyomi.data.backup.models.backupExtensionStoreMapper +import mihon.domain.extension.interactor.GetExtensionStores +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresBackupCreator( + private val getExtensionStores: GetExtensionStores = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getExtensionStores.get() + .map(backupExtensionStoreMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index cfb6198c2..7c37ef1d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -11,7 +11,7 @@ data class Backup( @ProtoNumber(101) var backupSources: List = emptyList(), @ProtoNumber(104) var backupPreferences: List = emptyList(), @ProtoNumber(105) var backupSourcePreferences: List = emptyList(), - @ProtoNumber(106) var backupExtensionRepo: List = emptyList(), + @ProtoNumber(106) var backupExtensionStores: List = emptyList(), // SY specific values @ProtoNumber(600) var backupSavedSearches: List = emptyList(), // KMK --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt deleted file mode 100644 index 256def7b5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.models - -import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoNumber -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -class BackupExtensionRepos( - @ProtoNumber(1) var baseUrl: String, - @ProtoNumber(2) var name: String, - @ProtoNumber(3) var shortName: String?, - @ProtoNumber(4) var website: String, - @ProtoNumber(5) var signingKeyFingerprint: String, -) - -val backupExtensionReposMapper = { repo: ExtensionRepo -> - BackupExtensionRepos( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt new file mode 100644 index 000000000..2304f260c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.backup.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@Serializable +class BackupExtensionStore( + @ProtoNumber(1) var indexUrl: String, + @ProtoNumber(2) var name: String, + @ProtoNumber(3) var badgeLabel: String?, + @ProtoNumber(5) var signingKey: String, + @ProtoNumber(4) var contactWebsite: String, + @ProtoNumber(6) var contactDiscord: String?, + @ProtoNumber(7) var isLegacy: Boolean?, +) + +val backupExtensionStoreMapper = { store: ExtensionStore -> + BackupExtensionStore( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt index 3f8d6ef22..9f578bbd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -5,14 +5,14 @@ import android.net.Uri import eu.kanade.tachiyomi.data.backup.BackupDecoder import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer -import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer @@ -38,7 +38,7 @@ class BackupRestorer( private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), - private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(), + private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(), private val mangaRestorer: MangaRestorer = MangaRestorer(isSync), // SY --> private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(), @@ -96,8 +96,8 @@ class BackupRestorer( if (options.appSettings) { restoreAmount += 1 } - if (options.extensionRepoSettings) { - restoreAmount += backup.backupExtensionRepo.size + if (options.extensionStores) { + restoreAmount += backup.backupExtensionStores.size } if (options.sourceSettings) { restoreAmount += 1 @@ -126,8 +126,8 @@ class BackupRestorer( if (options.libraryEntries) { restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList()) } - if (options.extensionRepoSettings) { - restoreExtensionRepos(backup.backupExtensionRepo) + if (options.extensionStores) { + restoreExtensionStores(backup.backupExtensionStores) } // TODO: optionally trigger online library + tracker update @@ -248,23 +248,25 @@ class BackupRestorer( } } - private fun CoroutineScope.restoreExtensionRepos( - backupExtensionRepo: List, + private fun CoroutineScope.restoreExtensionStores( + backupExtensionStores: List, ) = launch { - backupExtensionRepo + backupExtensionStores .forEach { ensureActive() try { - extensionRepoRestorer(it) + extensionStoreRestorer(it) } catch (e: Exception) { - errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}") + errors.add(Date() to "Error adding extension store: ${it.name} : ${e.message}") } restoreProgress += 1 + // KMK --> with(notifier) { + // KMK <-- showRestoreProgress( - context.stringResource(MR.strings.extensionRepo_settings), + context.stringResource(MR.strings.extensionStores), restoreProgress, restoreAmount, isSync, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 182b679d0..df62c7412 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -9,7 +9,7 @@ data class RestoreOptions( val libraryEntries: Boolean = true, val categories: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, // SY --> val savedSearchesFeeds: Boolean = true, @@ -20,7 +20,7 @@ data class RestoreOptions( libraryEntries, categories, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, // SY --> savedSearchesFeeds, @@ -31,7 +31,7 @@ data class RestoreOptions( libraryEntries || categories || appSettings || - extensionRepoSettings || + extensionStores || sourceSettings /* SY --> */ || savedSearchesFeeds /* SY <-- */ @@ -53,9 +53,9 @@ data class RestoreOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = RestoreOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = RestoreOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -77,7 +77,7 @@ data class RestoreOptions( libraryEntries = array[0], categories = array[1], appSettings = array[2], - extensionRepoSettings = array[3], + extensionStores = array[3], sourceSettings = array[4], // SY --> savedSearchesFeeds = array[5], diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt deleted file mode 100644 index 006e48df1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.restore.restorers - -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import tachiyomi.data.DatabaseHandler -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoRestorer( - private val handler: DatabaseHandler = Injekt.get(), - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), -) { - - suspend operator fun invoke( - backupRepo: BackupExtensionRepos, - ) { - val dbRepos = getExtensionRepos.getAll() - val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint } - val existingReposByUrl = dbRepos.associateBy { it.baseUrl } - - val urlExists = existingReposByUrl[backupRepo.baseUrl] - val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint] - - if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) { - error("Already Exists with different signing key fingerprint") - } else if (shaExists != null) { - error("${shaExists.name} has the same signing key fingerprint") - } else { - handler.await { - extension_reposQueries.insert( - backupRepo.baseUrl, - backupRepo.name, - backupRepo.shortName, - backupRepo.website, - backupRepo.signingKeyFingerprint, - ) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt new file mode 100644 index 000000000..a424cb3d0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import tachiyomi.data.Database +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoreRestorer( + private val database: Database = Injekt.get(), +) { + + suspend operator fun invoke( + backupStore: BackupExtensionStore, + ) { + // KMK --> + val indexUrl = if (backupStore.isLegacy == null) { + backupStore.indexUrl.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json" + } else { + backupStore.indexUrl + } + // KMK <-- + database.extension_storeQueries.upsert( + indexUrl = indexUrl, + name = backupStore.name, + badgeLabel = backupStore.badgeLabel ?: backupStore.name, + signingKey = backupStore.signingKey, + contactWebsite = backupStore.contactWebsite, + contactDiscord = backupStore.contactDiscord, + isLegacy = backupStore.isLegacy ?: true, + ) + } +} 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 15ed6a87d..d3f6b95b9 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 @@ -757,10 +757,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private const val KEY_MANGA_IDS = "manga_ids" // KMK <-- - fun cancelAllWorks(context: Context) { - context.workManager.cancelAllWorkByTag(TAG) - } - fun setupTask( context: Context, prefInterval: Int? = null, @@ -774,16 +770,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } else { NetworkType.CONNECTED } - val networkRequestBuilder = NetworkRequest.Builder() - if (DEVICE_ONLY_ON_WIFI in restrictions) { - networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - } - if (DEVICE_NETWORK_NOT_METERED in restrictions) { - networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + val networkRequest = NetworkRequest.Builder().apply { + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + if (DEVICE_ONLY_ON_WIFI in restrictions) { + addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + } + if (DEVICE_NETWORK_NOT_METERED in restrictions) { + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } } + .build() val constraints = Constraints.Builder() // 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used - .setRequiredNetworkRequest(networkRequestBuilder.build(), networkType) + .setRequiredNetworkRequest(networkRequest, networkType) .setRequiresCharging(DEVICE_CHARGING in restrictions) .setRequiresBatteryNotLow(true) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index ec289d2e7..1fa3c2929 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -88,7 +88,7 @@ class SyncManager( chapters = syncOptions.chapters, tracking = syncOptions.tracking, history = syncOptions.history, - extensionRepoSettings = syncOptions.extensionRepoSettings, + extensionStores = syncOptions.extensionStores, appSettings = syncOptions.appSettings, sourceSettings = syncOptions.sourceSettings, privateSettings = syncOptions.privateSettings, @@ -107,8 +107,8 @@ class SyncManager( backupCategories = backupCreator.backupCategories(backupOptions), backupSources = backupCreator.backupSources(backupManga), backupPreferences = backupCreator.backupAppPreferences(backupOptions), - backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions), backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions), + backupExtensionStores = backupCreator.backupExtensionStores(backupOptions), // SY --> backupSavedSearches = backupCreator.backupSavedSearches(backupOptions), @@ -192,7 +192,7 @@ class SyncManager( backupSources = remoteBackup.backupSources, backupPreferences = remoteBackup.backupPreferences, backupSourcePreferences = remoteBackup.backupSourcePreferences, - backupExtensionRepo = remoteBackup.backupExtensionRepo, + backupExtensionStores = remoteBackup.backupExtensionStores, // SY --> backupSavedSearches = remoteBackup.backupSavedSearches, @@ -222,7 +222,7 @@ class SyncManager( appSettings = true, sourceSettings = true, libraryEntries = true, - extensionRepoSettings = true, + extensionStores = true, ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt index ea04f72ac..3db50c859 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncYomiSyncService.kt @@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.await +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf @@ -21,7 +27,6 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.concurrent.TimeUnit class SyncYomiSyncService( context: Context, @@ -32,9 +37,34 @@ class SyncYomiSyncService( private val protoBuf: ProtoBuf = Injekt.get(), ) : SyncService(context, json, syncPreferences) { + // KMK --> + companion object { + private val client by lazy { OkHttpClient() } + } + // KMK <-- + private class SyncYomiException(message: String?) : Exception(message) + @Serializable + private data class SyncEvent( + val event: SyncEventStatus, + @SerialName("device_name") + val deviceName: String? = null, + val message: String? = null, + ) + + @Serializable + private enum class SyncEventStatus { + SYNC_STARTED, + SYNC_SUCCESS, + SYNC_FAILED, + SYNC_ERROR, + SYNC_CANCELLED, + } + override suspend fun doSync(syncData: SyncData): Backup? { + reportSyncEvent(SyncEventStatus.SYNC_STARTED) + try { val (remoteData, etag) = pullSyncData() @@ -52,11 +82,26 @@ class SyncYomiSyncService( syncData } - pushSyncData(finalSyncData, etag) + val success = pushSyncData(finalSyncData, etag) + + if (success) { + reportSyncEvent(SyncEventStatus.SYNC_SUCCESS) + } else { + reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data") + // KMK --> + return null + // KMK <-- + } + return finalSyncData.backup } catch (e: Exception) { + if (e is CancellationException) { + reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message) + throw e + } logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" } notifier.showSyncError(e.message) + reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message) return null } } @@ -78,7 +123,6 @@ class SyncYomiSyncService( headers = headers, ) - val client = OkHttpClient() val response = client.newCall(downloadRequest).await() if (response.code == HttpStatus.SC_NOT_MODIFIED) { @@ -123,13 +167,12 @@ class SyncYomiSyncService( /** * Return true if update success */ - private suspend fun pushSyncData(syncData: SyncData, eTag: String) { - val backup = syncData.backup ?: return + private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean { + val backup = syncData.backup ?: return true val host = syncPreferences.clientHost().get() val apiKey = syncPreferences.clientAPIKey().get() val uploadUrl = "$host/api/sync/content" - val timeout = 30L val headersBuilder = Headers.Builder().add("X-API-Token", apiKey) if (eTag.isNotEmpty()) { @@ -137,13 +180,6 @@ class SyncYomiSyncService( } val headers = headersBuilder.build() - // Set timeout to 30 seconds - val client = OkHttpClient.Builder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() - val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup) if (byteArray.isEmpty()) { throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error)) @@ -156,20 +192,58 @@ class SyncYomiSyncService( body = body, ) - val response = client.newCall(uploadRequest).await() + client.newCall(uploadRequest).await() + // KMK --> + .use { response -> + // KMK <-- + if (response.isSuccessful) { + val newETag = response.headers["ETag"] + .takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag") + syncPreferences.lastSyncEtag().set(newETag) + logcat(LogPriority.DEBUG) { "SyncYomi sync completed" } + return true + } else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) { + // other clients updated remote data, will try next time + logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" } + return false + } else { + val responseBody = response.body.string() + notifier.showSyncError("Failed to upload sync data: $responseBody") + logcat(LogPriority.ERROR) { "SyncError: $responseBody" } + return false + } + } + } - if (response.isSuccessful) { - val newETag = response.headers["ETag"] - .takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag") - syncPreferences.lastSyncEtag().set(newETag) - logcat(LogPriority.DEBUG) { "SyncYomi sync completed" } - } else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) { - // other clients updated remote data, will try next time - logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" } - } else { - val responseBody = response.body.string() - notifier.showSyncError("Failed to upload sync data: $responseBody") - logcat(LogPriority.ERROR) { "SyncError: $responseBody" } + private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) { + withContext(NonCancellable) { + try { + val host = syncPreferences.clientHost().get() + val apiKey = syncPreferences.clientAPIKey().get() + val url = "$host/api/sync/event" + + val headersBuilder = Headers.Builder().add("X-API-Token", apiKey) + val headers = headersBuilder.build() + + val bodyObj = SyncEvent( + event = event, + deviceName = android.os.Build.MODEL, + message = message, + ) + + val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj) + val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = POST( + url = url, + headers = headers, + body = requestBody, + ) + + client.newCall(request).await().close() + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" } + } } } } 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 0b6dca2b9..1ee49927e 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 @@ -5,6 +5,7 @@ import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALError import eu.kanade.tachiyomi.data.track.anilist.dto.ALIdSearchResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth @@ -26,6 +27,7 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.injectLazy import java.time.Instant @@ -43,6 +45,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { .rateLimit(permits = 85, period = 1.minutes) .build() + // KMK --> + private fun Response.parseALError() { + val bodyString = peekBody(1024 * 1024).string() + val errorObj = try { + json.decodeFromString(bodyString) + } catch (_: Exception) { + null + } + + errorObj?.errors?.firstOrNull()?.let { + val msg = it.message + if (msg.contains("Invalid token") || it.status == 401) { + throw Exception("AniList token expired, please login again") + } + throw Exception(msg) + } + } + // KMK <-- + suspend fun addLibManga(track: Track): Track { return withIOContext { val query = $$""" @@ -71,6 +92,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .let { track.library_id = it.data.entry.id @@ -112,6 +136,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() + // KMK --> + .use { it.parseALError() } + // KMK <-- track } } @@ -134,6 +161,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() + // KMK --> + .use { it.parseALError() } + // KMK <-- } } @@ -192,6 +222,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.page.media .map { it.toALManga().toTrack() } @@ -271,6 +304,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.page.mediaList .map { it.toALUserManga() } @@ -312,6 +348,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .let { val viewer = it.data.viewer @@ -323,9 +362,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata { return withIOContext { - val query = """ - |query (${'$'}mangaId: Int!) { - |Media (id: ${'$'}mangaId) { + val query = $$""" + |query ($mangaId: Int!) { + |Media (id: $mangaId) { |id |title { |userPreferred @@ -365,9 +404,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() - .let { - val media = it.data.media + .let { metadata -> + val media = metadata.data.media TrackMangaMetadata( remoteId = media.id, title = media.title.userPreferred, @@ -392,9 +434,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { // SY --> suspend fun searchById(id: String): TrackSearch { return withIOContext { - val query = """ - |query (${'$'}mangaId: Int!) { - |Media (id: ${'$'}mangaId) { + val query = $$""" + |query ($mangaId: Int!) { + |Media (id: $mangaId) { |id |title { |userPreferred @@ -430,6 +472,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { ), ) .awaitSuccess() + // KMK --> + .also { it.parseALError() } + // KMK <-- .parseAs() .data.media .toALManga() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt new file mode 100644 index 000000000..7376cb881 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALError.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ALError( + val errors: List? = null, +) + +@Serializable +data class ALErrorItem( + val message: String, + val status: Int? = null, +) 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 d936a615a..147331dbf 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 @@ -14,7 +14,9 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.util.PkceUtil @@ -124,8 +126,23 @@ class MyAnimeListApi( .put(formBodyBuilder.build()) .build() with(json) { - authClient.newCall(request) - .awaitSuccess() + val response = authClient + .newCall(request) + .await() + + if (!response.isSuccessful) { + if (response.body.string().contains("invalid_content")) { + // MAL returns unapproved titles in search but does not allow adding them to the list + // returns 400 with this body: {"message":"Invalid content","error":"invalid_content"} + // These unapproved titles cannot be filtered out in search and are also returned by the + // endpoint we use for id prefix search + throw MALTitleNotApproved() + } else { + throw HttpException(response.code) + } + } + + response .parseAs() .let { parseMangaItem(it, track) } } 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 09d56f266..5620e0ad0 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 @@ -80,5 +80,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor } } +class MALTitleNotApproved : IOException("MAL: This title can't be added because it is waiting for approval.") class MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token") class MALTokenExpired : IOException("MAL: Login has expired") 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 f2fd81758..1391c9263 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -200,7 +200,7 @@ class ExtensionManager( availableExtensionMapFlow.value = extensions.associateBy { it.pkgName + // KMK --> - ":${it.signatureHash}" + "_${it.signatureHash}" // KMK <-- } updatedInstalledExtensionsStatuses(extensions) @@ -273,14 +273,14 @@ class ExtensionManager( // Ext found: Update installed extensions with new information from repo // Also clear isObsolete and set new repo Name if needed val hasUpdate = extension.updateExists(availableExt) + // KMK --> installedExtensionsMap[pkgName] = extension.copy( hasUpdate = hasUpdate, - repoUrl = availableExt.repoUrl, - // KMK --> + store = availableExt.store, isObsolete = false, - repoName = extension.repoName ?: availableExt.repoName, - // KMK <-- + storeName = extension.storeName ?: availableExt.storeName, ) + // KMK <-- changed = true } } @@ -298,7 +298,7 @@ class ExtensionManager( * @param extension The extension to be installed. */ fun installExtension(extension: Extension.Available): Flow { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) + return installer.downloadAndInstall(extension.apkUrl, extension) } /** @@ -312,7 +312,7 @@ class ExtensionManager( val availableExt = availableExtensionMapFlow.value[ extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- ] ?: return emptyFlow() return installExtension(availableExt) @@ -322,7 +322,7 @@ class ExtensionManager( installer.cancelInstall( extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- ) } 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 1cefa8afd..5c7f0f18a 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 @@ -6,24 +6,13 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.parseAs import exh.source.BlacklistedSources import exh.source.ExhPreferences -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import logcat.LogPriority -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -32,19 +21,16 @@ import kotlin.time.Duration.Companion.days internal class ExtensionApi { - private val networkService: NetworkHelper by injectLazy() + private val repository: ExtensionStoreRepository by injectLazy() + private val preferenceStore: PreferenceStore by injectLazy() - private val getExtensionRepo: GetExtensionRepo by injectLazy() - private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() + private val updateExtensionStores: UpdateExtensionStores by injectLazy() private val extensionManager: ExtensionManager by injectLazy() // SY --> private val sourcePreferences: SourcePreferences by injectLazy() - // SY <-- - private val json: Json by injectLazy() - private val lastExtCheck: Preference by lazy { preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } @@ -54,37 +40,11 @@ internal class ExtensionApi { val disabledRepos = sourcePreferences.disabledRepos().get() // KMK <-- return withIOContext { - getExtensionRepo.getAll() + repository.fetchExtensions( // KMK --> - .filterNot { it.baseUrl in disabledRepos } + disabledRepos, // KMK <-- - .map { async { getExtensions(it) } } - .awaitAll() - .flatten() - } - } - - private suspend fun getExtensions(extRepo: ExtensionRepo): List { - val repoBaseUrl = extRepo.baseUrl - return try { - val response = networkService.client - .newCall(GET("$repoBaseUrl/index.min.json")) - .awaitSuccess() - - with(json) { - response - .parseAs>() - .toExtensions( - repoBaseUrl, - // KMK --> - signature = extRepo.signingKeyFingerprint, - repoName = extRepo.shortName ?: extRepo.name, - // KMK <-- - ) - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" } - emptyList() + ) } } @@ -99,8 +59,7 @@ internal class ExtensionApi { return null } - // Update extension repo details - updateExtensionRepo.awaitAll() + updateExtensionStores() val extensions = if (fromAvailableExtensionList) { extensionManager.availableExtensionsFlow.value @@ -138,47 +97,6 @@ internal class ExtensionApi { return extensionsWithUpdate } - private fun List.toExtensions( - repoUrl: String, - // KMK --> - signature: String, - repoName: String, - // KMK <-- - ): List { - return this - .filter { - val libVersion = it.extractLibVersion() - libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX - } - .map { - Extension.Available( - name = it.name.substringAfter("Tachiyomi: "), - pkgName = it.pkg, - versionName = it.version, - versionCode = it.code, - libVersion = it.extractLibVersion(), - lang = it.lang, - isNsfw = it.nsfw == 1, - sources = it.sources?.map(extensionSourceMapper).orEmpty(), - apkName = it.apk, - iconUrl = "$repoUrl/icon/${it.pkg}.png", - repoUrl = repoUrl, - // KMK --> - signatureHash = signature, - repoName = repoName, - // KMK <-- - ) - } - } - - fun getApkUrl(extension: Extension.Available): String { - return "${extension.repoUrl}/apk/${extension.apkName}" - } - - private fun ExtensionJsonObject.extractLibVersion(): Double { - return version.substringBeforeLast('.').toDouble() - } - // SY --> private fun Extension.isBlacklisted( blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(), @@ -194,32 +112,3 @@ internal class ExtensionApi { } // SY <-- } - -@Serializable -private data class ExtensionJsonObject( - val name: String, - val pkg: String, - val apk: String, - val lang: String, - val code: Long, - val version: String, - val nsfw: Int, - val sources: List?, -) - -@Serializable -private data class ExtensionSourceJsonObject( - val id: Long, - val lang: String, - val name: String, - val baseUrl: String, -) - -private val extensionSourceMapper: (ExtensionSourceJsonObject) -> Extension.Available.Source = { - Extension.Available.Source( - id = it.id, - lang = it.lang, - name = it.name, - baseUrl = it.baseUrl, - ) -} 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 index 3a2200632..645a37d42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -24,7 +24,9 @@ abstract class Installer(private val service: Service) { private val extensionManager: ExtensionManager by injectLazy() private var waitingInstall = AtomicReference(null) - private val queue = Collections.synchronizedList(mutableListOf()) + // KMK --> + private val queue = Collections.synchronizedSet(mutableSetOf()) + // KMK <-- private val cancelReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -104,7 +106,9 @@ abstract class Installer(private val service: Service) { } val nextEntry = queue.first() if (waitingInstall.compareAndSet(null, nextEntry)) { - queue.removeAt(0) + // KMK --> + queue.remove(nextEntry) + // KMK <-- processEntry(nextEntry) } } @@ -129,7 +133,11 @@ abstract class Installer(private val service: Service) { */ private fun cancelQueue(downloadId: Long) { val waitingInstall = this.waitingInstall.load() - val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return + // KMK --> + val toCancel = synchronized(queue) { queue.find { it.downloadId == downloadId } } + ?: waitingInstall?.takeIf { it.downloadId == downloadId } + // KMK <-- + ?: return if (cancelEntry(toCancel)) { queue.remove(toCancel) if (waitingInstall == toCancel) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index d3aef3b18..a3e37a6aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.installer +import android.annotation.SuppressLint import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver @@ -14,8 +15,7 @@ import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat import eu.kanade.tachiyomi.util.system.getUriSize -import logcat.LogPriority -import tachiyomi.core.common.util.system.logcat +import exh.log.xLogE class PackageInstallerInstaller(private val service: Service) : Installer(service) { @@ -41,7 +41,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic .sanitizeByFiltering(this) } if (userAction == null) { - logcat(LogPriority.ERROR) { "Fatal error for $intent" } + xLogE("Fatal error for $intent") continueQueue(InstallStep.Error) return } @@ -82,6 +82,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic inputStream.copyTo(outputStream) session.fsync(outputStream) } + service.contentResolver.delete(entry.uri, null, null) val intentSender = PendingIntent.getBroadcast( service, @@ -89,10 +90,11 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic Intent(INSTALL_ACTION).setPackage(service.packageName), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, ).intentSender + @SuppressLint("RequestInstallPackagesPolicy") session.commit(intentSender) } } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } + xLogE("Failed to install extension ${entry.downloadId} ${entry.uri}", e) activeSession?.let { (_, sessionId) -> packageInstaller.abandonSession(sessionId) } @@ -103,8 +105,13 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic override fun cancelEntry(entry: Entry): Boolean { activeSession?.let { (activeEntry, sessionId) -> if (activeEntry == entry) { - packageInstaller.abandonSession(sessionId) - return false + return try { + packageInstaller.abandonSession(sessionId) + false + } catch (_: SecurityException) { + // Highly likely the session has succeeded + true + } } } return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index b9e175577..8eae5277f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -111,20 +111,17 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { override fun processEntry(entry: Entry) { super.processEntry(entry) - - // KMK --> - val installer = shellInterface - if (installer == null) { - logcat(LogPriority.ERROR) { "Shizuku shell interface not available for ${entry.downloadId}" } - continueQueue(InstallStep.Error) - return - } - try { service.contentResolver.openAssetFileDescriptor(entry.uri, "r")?.use { - installer.install(it) + shellInterface?.install(it) + // KMK --> + ?: throw Exception("Shell interface is not available") + // KMK <-- } + // KMK --> + ?: throw Exception("Failed to open asset file descriptor") // KMK <-- + service.contentResolver.delete(entry.uri, null, null) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" } continueQueue(InstallStep.Error) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt index a300ba2ea..ef73e9de7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt @@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() { } } + override fun onDestroy() { + super.onDestroy() + intent.data?.let { contentResolver.delete(it, null, null) } + } + private fun checkInstallationResult(resultCode: Int) { val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID) val extensionManager = Injekt.get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt index dab5df61e..cb3fc564e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt @@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.extension.util import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.graphics.BitmapFactory import android.net.Uri +import android.os.Build import android.os.IBinder import androidx.core.content.ContextCompat import eu.kanade.domain.base.BasePreferences @@ -16,11 +18,15 @@ import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat import eu.kanade.tachiyomi.util.system.notificationBuilder -import logcat.LogPriority +import exh.log.xLogE import tachiyomi.core.common.i18n.stringResource -import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR +/** + * Foreground service that stages and installs extension APKs via [PackageInstallerInstaller] or + * [ShizukuInstaller]. Uses [ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC] because installs can + * wait on multiple system confirmation dialogs; `shortService` times out after ~3 minutes. + */ class ExtensionInstallService : Service() { private var installer: Installer? = null @@ -36,7 +42,17 @@ class ExtensionInstallService : Service() { setContentTitle(stringResource(MR.strings.ext_install_service_notif)) setProgress(100, 100, true) }.build() - startForeground(Notifications.ID_EXTENSION_INSTALLER, notification) + // KMK --> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + Notifications.ID_EXTENSION_INSTALLER, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) + } else { + // KMK <-- + startForeground(Notifications.ID_EXTENSION_INSTALLER, notification) + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -53,7 +69,7 @@ class ExtensionInstallService : Service() { BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this) BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this) else -> { - logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" } + xLogE("Not implemented for installer $installerUsed") stopSelf() return START_NOT_STICKY } 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 6b471ba3b..2995ae1de 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 @@ -1,66 +1,52 @@ package eu.kanade.tachiyomi.extension.util -import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Environment import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService import androidx.core.net.toUri import eu.kanade.domain.base.BasePreferences -import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.installer.Installer import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.isPackageInstalled -import kotlinx.coroutines.delay +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.launch import logcat.LogPriority -import tachiyomi.core.common.util.lang.withUIContext +import okhttp3.OkHttpClient +import okhttp3.Request import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import kotlin.time.Duration.Companion.seconds +import java.util.concurrent.ConcurrentHashMap /** * The installer which installs, updates and uninstalls the extensions. * * @param context The application context. */ -internal class ExtensionInstaller(private val context: Context) { - - /** - * The system's download manager - */ - private val downloadManager = context.getSystemService()!! - - /** - * The broadcast receiver which listens to download completion events. - */ - private val downloadReceiver = DownloadCompletionReceiver() - - /** - * The currently requested downloads, with the package name (unique id) as key, and the id - * returned by the download manager. - */ - private val activeDownloads = hashMapOf() - - private val downloadsStateFlows = hashMapOf>() +internal class ExtensionInstaller( + private val context: Context, +) { + // KMK --> + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val activeJobs = ConcurrentHashMap() + private val activeSteps = ConcurrentHashMap>() + // KMK <-- private val extensionInstaller = Injekt.get().extensionInstaller() + private val httpClient: OkHttpClient = Injekt.get().client + /** * Adds the given extension to the downloads queue and returns an observable containing its * step in the installation process. @@ -71,130 +57,97 @@ internal class ExtensionInstaller(private val context: Context) { fun downloadAndInstall(url: String, extension: Extension): Flow { val pkgName = extension.pkgName + // KMK --> - ":${extension.signatureHash}" + "_${extension.signatureHash}" + val downloadId = pkgName.toDownloadId() // KMK <-- + cancelInstall(pkgName) - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } + val step = MutableStateFlow(InstallStep.Pending) + activeSteps[downloadId] = step - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() + val job = scope.launch { + val tmpFile = File(context.cacheDir, "extension_$pkgName.apk") + try { + step.value = InstallStep.Downloading + val request = Request.Builder().url(url).build() + httpClient.newCall(request).execute() + // KMK --> + .use { response -> + // KMK <-- - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + if (!response.isSuccessful) { + throw Exception("Failed to download extension") + } + // KMK --> + tmpFile.outputStream().use { output -> + response.body.byteStream().use { input -> + // KMK <-- + input.copyTo(output) + } + } + } - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - val downloadStateFlow = MutableStateFlow(InstallStep.Pending) - downloadsStateFlows[id] = downloadStateFlow - - // Poll download status - val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus -> - // Map to our model - when (downloadStatus) { - DownloadManager.STATUS_PENDING -> InstallStep.Pending - DownloadManager.STATUS_RUNNING -> InstallStep.Downloading - else -> null + step.value = InstallStep.Installing + installApk(downloadId, tmpFile) + } catch (e: Exception) { + if (e is InterruptedException) { + // Canceled + } else { + logcat(LogPriority.ERROR, e) + step.value = InstallStep.Error + } + // KMK --> + tmpFile.delete() + // KMK <-- } } - return merge(downloadStateFlow, pollStatusFlow).transformWhile { - emit(it) - // Stop when the application is installed or errors - !it.isCompleted() - }.onCompletion { - // Always notify on main thread - withUIContext { - // Always remove the download when unsubscribed - deleteDownload(pkgName) + activeJobs[pkgName] = job + + return step.asStateFlow() + .onCompletion { + activeJobs.remove(pkgName) + activeSteps.remove(downloadId) + job.cancel() } - } } - /** - * Returns a flow that polls the given download id for its status every second, as the - * manager doesn't have any notification system. It'll stop once the download finishes. - * - * @param id The id of the download to poll. - */ - private fun downloadStatusFlow(id: Long): Flow = flow { - val query = DownloadManager.Query().setFilterById(id) - while (true) { - // Get the current download status - val downloadStatus = downloadManager.query(query).use { cursor -> - if (!cursor.moveToFirst()) return@flow - cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) - } - - emit(downloadStatus) - - // Stop polling when the download fails or finishes - if ( - downloadStatus == DownloadManager.STATUS_SUCCESSFUL || - downloadStatus == DownloadManager.STATUS_FAILED - ) { - return@flow - } - - delay(1.seconds) - } - } - // Ignore duplicate results - .distinctUntilChanged() - /** * Starts an intent to install the extension at the given uri. * - * @param uri The uri of the extension to install. + * @param tempFile The file of the extension to install. Delete after use. */ - fun installApk(downloadId: Long, uri: Uri) { + private fun installApk(downloadId: Long, tempFile: File) { when (val installer = extensionInstaller.get()) { BasePreferences.ExtensionInstaller.LEGACY -> { val intent = Intent(context, ExtensionInstallActivity::class.java) - .setDataAndType(uri, APK_MIME) + .setDataAndType(tempFile.getUriCompat(context), APK_MIME) .putExtra(EXTRA_DOWNLOAD_ID, downloadId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) context.startActivity(intent) } BasePreferences.ExtensionInstaller.PRIVATE -> { - val extensionManager = Injekt.get() - val tempFile = File(context.cacheDir, "temp_$downloadId") - - if (tempFile.exists() && !tempFile.delete()) { - // Unlikely but just in case - extensionManager.updateInstallStep(downloadId, InstallStep.Error) - return - } - try { - context.contentResolver.openInputStream(uri)?.use { input -> - tempFile.outputStream().use { output -> - input.copyTo(output) - } - } - if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) { - extensionManager.updateInstallStep(downloadId, InstallStep.Installed) + updateInstallStep(downloadId, InstallStep.Installed) } else { - extensionManager.updateInstallStep(downloadId, InstallStep.Error) + updateInstallStep(downloadId, InstallStep.Error) } } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." } - extensionManager.updateInstallStep(downloadId, InstallStep.Error) + updateInstallStep(downloadId, InstallStep.Error) } tempFile.delete() } else -> { - val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer) + val intent = ExtensionInstallService.getIntent( + context, + downloadId, + tempFile.getUriCompat(context), + installer, + ) ContextCompat.startForegroundService(context, intent) } } @@ -204,9 +157,8 @@ internal class ExtensionInstaller(private val context: Context) { * Cancels extension install and remove from download manager and installer. */ fun cancelInstall(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) ?: return - downloadManager.remove(downloadId) - Installer.cancelInstallQueue(context, downloadId) + activeJobs.remove(pkgName)?.cancel() + Installer.cancelInstallQueue(context, /* KMK --> */ pkgName.toDownloadId() /* KMK <-- */) } /** @@ -233,91 +185,16 @@ internal class ExtensionInstaller(private val context: Context) { * @param step New install step. */ fun updateInstallStep(downloadId: Long, step: InstallStep) { - downloadsStateFlows[downloadId]?.let { it.value = step } - } - - /** - * Deletes the download for the given package name. - * - * @param pkgName The package name of the download to delete together with its signature. - */ - private fun deleteDownload(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) - if (downloadId != null) { - downloadManager.remove(downloadId) - downloadsStateFlows.remove(downloadId) - } - if (activeDownloads.isEmpty()) { - downloadReceiver.unregister() - } - } - - /** - * Receiver that listens to download status events. - */ - private inner class DownloadCompletionReceiver : BroadcastReceiver() { - - /** - * Whether this receiver is currently registered. - */ - private var isRegistered = false - - /** - * Registers this receiver if it's not already. - */ - fun register() { - if (isRegistered) return - isRegistered = true - - val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) - } - - /** - * Unregisters this receiver if it's not already. - */ - fun unregister() { - if (!isRegistered) return - isRegistered = false - - context.unregisterReceiver(this) - } - - /** - * Called when a download event is received. It looks for the download in the current active - * downloads and notifies its installation step. - */ - override fun onReceive(context: Context, intent: Intent?) { - val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return - - // Avoid events for downloads we didn't request - if (id !in activeDownloads.values) return - - val uri = downloadManager.getUriForDownloadedFile(id) - - // Set next installation step - if (uri == null) { - logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" } - updateInstallStep(id, InstallStep.Error) - return - } - - val query = DownloadManager.Query().setFilterById(id) - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - val localUri = cursor.getString( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), - ).removePrefix(FILE_SCHEME) - - installApk(id, File(localUri).getUriCompat(context)) - } - } - } + activeSteps[downloadId]?.let { it.value = step } } companion object { const val APK_MIME = "application/vnd.android.package-archive" const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" - const val FILE_SCHEME = "file://" + + // KMK --> + /** Convert packageName to download ID avoiding negative number */ + private fun String.toDownloadId(): Long = hashCode().toLong() and 0xFFFFFFFFL + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 78866756d..b72b6c9c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import logcat.LogPriority -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.model.ExtensionStore import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.io.File @@ -46,7 +46,7 @@ internal object ExtensionLoader { private val trustExtension: TrustExtension by injectLazy() // KMK --> - private val getExtensionRepo: GetExtensionRepo by injectLazy() + private val getExtensionStores: GetExtensionStores by injectLazy() // KMK <-- private val loadNsfwSource by lazy { @@ -169,7 +169,7 @@ internal object ExtensionLoader { // Load each extension concurrently and wait for completion return runBlocking { // KMK --> - val extRepos = getExtensionRepo.getAll() + val extStores = getExtensionStores.get() // KMK <-- val deferred = extPkgs.map { async { @@ -177,7 +177,7 @@ internal object ExtensionLoader { context, it, // KMK --> - extRepos, + extStores, // KMK <-- ) } @@ -228,7 +228,7 @@ internal object ExtensionLoader { isShared = true, ) } - } catch (error: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { null } @@ -245,11 +245,11 @@ internal object ExtensionLoader { context: Context, extensionInfo: ExtensionInfo, // KMK --> - extRepos: List? = null, + extStores: List? = null, // KMK <-- ): LoadResult { // KMK --> - val repos = extRepos ?: getExtensionRepo.getAll() + val stores = extStores ?: getExtensionStores.get() // KMK <-- val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo @@ -288,10 +288,10 @@ internal object ExtensionLoader { libVersion, signatures.last(), // KMK --> - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) @@ -358,10 +358,10 @@ internal object ExtensionLoader { isShared = extensionInfo.isShared, // KMK --> signatureHash = signatures.last(), - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index e5280bc02..52ea950c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -13,9 +13,7 @@ import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino import exh.log.xLogD import exh.source.BlacklistedSources import exh.source.DelegatedHttpSource @@ -24,10 +22,8 @@ import exh.source.EIGHTMUSES_SOURCE_ID import exh.source.EXHENTAI_EXT_SOURCES import exh.source.EnhancedHttpSource import exh.source.ExhPreferences -import exh.source.HBROWSE_SOURCE_ID import exh.source.MERGED_SOURCE_ID import exh.source.PURURIN_SOURCE_ID -import exh.source.TSUMINO_SOURCE_ID import exh.source.handleSourceLibrary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -282,12 +278,6 @@ class AndroidSourceManager( "eu.kanade.tachiyomi.extension.en.pururin.Pururin", Pururin::class, ), - DelegatedSource( - "Tsumino", - TSUMINO_SOURCE_ID, - "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", - Tsumino::class, - ), DelegatedSource( "MangaDex", fillInSourceId, @@ -295,12 +285,6 @@ class AndroidSourceManager( MangaDex::class, true, ), - DelegatedSource( - "HBrowse", - HBROWSE_SOURCE_ID, - "eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse", - HBrowse::class, - ), DelegatedSource( "8Muses", EIGHTMUSES_SOURCE_ID, diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt deleted file mode 100644 index 3712395ee..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HBrowse.kt +++ /dev/null @@ -1,95 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.content.Context -import android.net.Uri -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.MetadataSource -import eu.kanade.tachiyomi.source.online.NamespaceSource -import eu.kanade.tachiyomi.source.online.UrlImportableSource -import eu.kanade.tachiyomi.util.asJsoup -import exh.metadata.metadata.HBrowseSearchMetadata -import exh.metadata.metadata.base.RaisedTag -import exh.source.DelegatedHttpSource -import exh.util.urlImportFetchSearchManga -import exh.util.urlImportFetchSearchMangaSuspend -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element - -class HBrowse(delegate: HttpSource, val context: Context) : - DelegatedHttpSource(delegate), - MetadataSource, - UrlImportableSource, - NamespaceSource { - override val metaClass = HBrowseSearchMetadata::class - override fun newMetaInstance() = HBrowseSearchMetadata() - override val lang = "en" - - // Support direct URL importing - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = - urlImportFetchSearchManga(context, query) { - @Suppress("DEPRECATION") - super.fetchSearchManga(page, query, filters) - } - - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return urlImportFetchSearchMangaSuspend(context, query) { - super.getSearchManga(page, query, filters) - } - } - - override suspend fun getMangaDetails(manga: SManga): SManga { - val response = client.newCall(mangaDetailsRequest(manga)).awaitSuccess() - return parseToManga(manga, response.asJsoup()) - } - - override suspend fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) { - val tables = parseIntoTables(input) - with(metadata) { - hbUrl = input.location().removePrefix("$baseUrl/thumbnails") - - hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong() - - tags.clear() - ((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) -> - when (val lowercaseNs = k.lowercase()) { - "title" -> title = v.text() - "length" -> length = v.text().substringBefore(" ").toInt() - else -> { - v.getElementsByTag("a").forEach { - tags += RaisedTag( - lowercaseNs, - it.text(), - HBrowseSearchMetadata.TAG_TYPE_DEFAULT, - ) - } - } - } - } - } - } - - private fun parseIntoTables(doc: Document): Map> { - return doc.select("#main > .listTable").associate { ele -> - val tableName = ele.previousElementSibling()?.text()?.lowercase().orEmpty() - tableName to ele.select("tr") - .filter { element -> element.childrenSize() > 1 } - .associate { - it.child(0).text() to it.child(1) - } - } - } - - override val matchingHosts = listOf( - "www.hbrowse.com", - "hbrowse.com", - ) - - override suspend fun mapUrlToMangaUrl(uri: Uri): String? { - return uri.pathSegments.firstOrNull()?.let { "/$it/c00001/" } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt deleted file mode 100644 index b39aead96..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt +++ /dev/null @@ -1,151 +0,0 @@ -package eu.kanade.tachiyomi.source.online.english - -import android.content.Context -import android.net.Uri -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.MetadataSource -import eu.kanade.tachiyomi.source.online.NamespaceSource -import eu.kanade.tachiyomi.source.online.UrlImportableSource -import eu.kanade.tachiyomi.util.asJsoup -import exh.metadata.metadata.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL -import exh.metadata.metadata.TsuminoSearchMetadata -import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT -import exh.metadata.metadata.base.RaisedTag -import exh.source.DelegatedHttpSource -import exh.util.dropBlank -import exh.util.trimAll -import exh.util.urlImportFetchSearchManga -import exh.util.urlImportFetchSearchMangaSuspend -import org.jsoup.nodes.Document -import rx.Observable -import java.text.SimpleDateFormat -import java.util.Locale - -class Tsumino(delegate: HttpSource, val context: Context) : - DelegatedHttpSource(delegate), - MetadataSource, - UrlImportableSource, - NamespaceSource { - override val metaClass = TsuminoSearchMetadata::class - override fun newMetaInstance() = TsuminoSearchMetadata() - override val lang = "en" - - // Support direct URL importing - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = - urlImportFetchSearchManga(context, query) { - @Suppress("DEPRECATION") - super.fetchSearchManga(page, query, filters) - } - - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - return urlImportFetchSearchMangaSuspend(context, query) { - super.getSearchManga(page, query, filters) - } - } - - override suspend fun mapUrlToMangaUrl(uri: Uri): String? { - val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.lowercase(Locale.ROOT) ?: return null - if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") { - return null - } - return "https://tsumino.com/Book/Info/${uri.lastPathSegment}" - } - - override suspend fun getMangaDetails(manga: SManga): SManga { - val response = client.newCall(mangaDetailsRequest(manga)).awaitSuccess() - return parseToManga(manga, response.asJsoup()) - } - - override suspend fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) { - with(metadata) { - tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location())!!.toInt() - tags.clear() - - input.select("meta[property=og:title]").firstOrNull()?.attr("content")?.let { - title = it.trim() - } - - input.getElementById("Artist")?.children()?.first()?.attr("data-define")?.trim()?.let { artistString -> - artistString.split("|").trimAll().dropBlank().forEach { - tags.add(RaisedTag("artist", it, TAG_TYPE_DEFAULT)) - } - tags.add(RaisedTag("artist", artistString, TAG_TYPE_VIRTUAL)) - artist = artistString - } - - input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let { - uploader = it - } - - input.getElementById("Uploaded")?.text()?.let { - uploadDate = TM_DATE_FORMAT.parse(it.trim())!!.time - } - - input.getElementById("Pages")?.text()?.let { - length = it.trim().toIntOrNull() - } - - input.getElementById("Rating")?.text()?.let { - ratingString = it.trim() - val ratingString = ratingString - if (!ratingString.isNullOrBlank()) { - averageRating = RATING_FLOAT_REGEX.find(ratingString)?.groups?.get(1)?.value?.toFloatOrNull() - userRatings = RATING_USERS_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull() - favorites = RATING_FAVORITES_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull() - } - } - - input.getElementById("Category")?.children()?.first()?.attr("data-define")?.let { - category = it.trim() - tags.add(RaisedTag("genre", it, TAG_TYPE_VIRTUAL)) - } - - input.getElementById("Collection")?.children()?.first()?.attr("data-define")?.let { - collection = it.trim() - tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT)) - } - - input.getElementById("Group")?.children()?.first()?.attr("data-define")?.let { - group = it.trim() - tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT)) - } - - parody = input.getElementById("Parody")?.children()?.map { - val entry = it.attr("data-define").trim() - tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT)) - entry - }.orEmpty() - - character = input.getElementById("Character")?.children()?.map { - val entry = it.attr("data-define").trim() - tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT)) - entry - }.orEmpty() - - input.getElementById("Tag")?.children()?.let { tagElements -> - tags.addAll( - tagElements.map { - RaisedTag("tags", it.attr("data-define").trim(), TAG_TYPE_DEFAULT) - }, - ) - } - } - } - - override val matchingHosts = listOf( - "www.tsumino.com", - "tsumino.com", - ) - - companion object { - val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US) - val RATING_FLOAT_REGEX = "([0-9].*) \\(".toRegex() - val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex() - val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt index d1669f879..659cb7cd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt @@ -59,7 +59,7 @@ class ExtensionsScreen(private val searchSource: String? = null) : Screen() { /* * This will redo the searching for [searchSource] every times the screen is launched, for example when - * back from the [ExtensionFilterScreen] or from the [ExtensionReposScreen]. + * back from the [ExtensionFilterScreen] or from the [ExtensionStoresScreen]. * Not really desired but let's accept it. */ if (!searchSource.isNullOrBlank()) onChangeSearchQuery(searchSource) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index d3768217c..fbaa435ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tachiyomi.core.common.util.lang.launchIO @@ -53,7 +54,7 @@ class ExtensionsScreenModel( map[ it.pkgName + // KMK --> - ":${it.signatureHash}", + "_${it.signatureHash}", // KMK <-- ] ?: InstallStep.Idle, ) @@ -207,7 +208,7 @@ class ExtensionsScreenModel( it + Pair( extension.pkgName + // KMK --> - ":${extension.signatureHash}", + "_${extension.signatureHash}", // KMK <-- installStep, ) @@ -219,7 +220,7 @@ class ExtensionsScreenModel( it - ( extension.pkgName + // KMK --> - ":${extension.signatureHash}" + "_${extension.signatureHash}" // KMK <-- ) } @@ -228,6 +229,7 @@ class ExtensionsScreenModel( private suspend fun Flow.collectToInstallUpdate(extension: Extension) = this .onEach { installStep -> addDownloadState(extension, installStep) } + .takeWhile { installStep -> installStep != InstallStep.Installed } .onCompletion { removeDownloadState(extension) } .collect() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index ac9d8e920..1f4b1a244 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -20,7 +20,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.ExtensionScreen import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen @@ -62,8 +62,8 @@ fun extensionsTab( onClick = { navigator.push(ExtensionFilterScreen()) }, ), AppBar.OverflowAction( - title = stringResource(MR.strings.label_extension_repos), - onClick = { navigator.push(ExtensionReposScreen()) }, + title = stringResource(MR.strings.extensionStores), + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), content = { contentPadding, _ -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index fc7811db8..003d69a69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.os.bundleOf import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentManager @@ -182,7 +181,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { fun getInstance(sourceId: Long): SourcePreferencesFragment { return SourcePreferencesFragment().apply { - arguments = bundleOf(SOURCE_ID to sourceId) + arguments = Bundle().apply { + putLong(SOURCE_ID, sourceId) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt index 4dc5b482f..670b88ec0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt @@ -128,11 +128,9 @@ data class MigrateMangaScreen( // KMK --> bottomBar = { MigrateMangaBottomBar( - selected = state.selection, + selectionMode = state.selectionMode, onMultiMigrateClicked = { - val selection = state.selection - .map { it.manga.id } - navigator.push(MigrationConfigScreen(selection)) + navigator.push(MigrationConfigScreen(state.selection)) }, enableScrollToTop = enableScrollToTop, enableScrollToBottom = enableScrollToBottom, @@ -187,7 +185,7 @@ data class MigrateMangaScreen( contentPadding: PaddingValues, state: MigrateMangaScreenModel.State, // KMK --> - onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit, + onMangaSelected: (Manga, Boolean, Boolean) -> Unit, // KMK <-- onClickItem: (Manga) -> Unit, onClickCover: (Manga) -> Unit, @@ -197,20 +195,23 @@ data class MigrateMangaScreen( contentPadding = contentPadding, ) { items(items = state.titles) { manga -> + // KMK --> + val isSelected = manga.id in state.selection + // KMK <-- MigrateMangaItem( - manga = manga.manga, - isSelected = manga.selected, + manga = manga, + isSelected = isSelected, onClickItem = { // KMK --> when { - state.selectionMode -> onMangaSelected(manga, !manga.selected, true, false) + state.selectionMode -> onMangaSelected(manga, !isSelected, false) // KMK <-- else -> onClickItem(it) } }, onClickCover = onClickCover, // KMK --> - onLongClick = { onMangaSelected(manga, !manga.selected, true, true) }, + onLongClick = { onMangaSelected(manga, !isSelected, true) }, modifier = Modifier.animateItemFastScroll(), // KMK <-- ) @@ -293,7 +294,7 @@ data class MigrateMangaScreen( @Composable private fun MigrateMangaBottomBar( modifier: Modifier = Modifier, - selected: List, + selectionMode: Boolean, onMultiMigrateClicked: () -> Unit, enableScrollToTop: Boolean, enableScrollToBottom: Boolean, @@ -302,7 +303,7 @@ data class MigrateMangaScreen( ) { val scope = rememberCoroutineScope() val animatedElevation by animateDpAsState( - targetValue = if (selected.isNotEmpty()) 3.dp else 0.dp, + targetValue = if (selectionMode) 3.dp else 0.dp, label = "elevation", ) Surface( @@ -350,7 +351,7 @@ data class MigrateMangaScreen( toConfirm = confirm[1], onLongClick = { onLongClickItem(1) }, onClick = onMultiMigrateClicked, - enabled = selected.isNotEmpty(), + enabled = selectionMode, ) Button( title = stringResource(KMR.strings.action_scroll_to_bottom), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt index d9e2a8f3b..b2e4e734a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga import androidx.compose.runtime.Immutable import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.core.util.addOrRemove import eu.kanade.tachiyomi.source.Source import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -17,6 +16,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.core.common.utils.mutate import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.model.Manga @@ -36,7 +36,6 @@ class MigrateMangaScreenModel( // KMK --> // First and last selected index in list private val selectedPositions: Array = arrayOf(-1, -1) - private val selectedMangaIds: HashSet = HashSet() // KMK <-- init { @@ -50,120 +49,133 @@ class MigrateMangaScreenModel( logcat(LogPriority.ERROR, it) _events.send(MigrationMangaEvent.FailedFetchingFavorites) mutableState.update { state -> - state.copy(titleList = persistentListOf()) + state.copy( + titleList = persistentListOf(), + // KMK --> + selection = emptySet(), + // KMK <-- + ) } } - // KMK --> - .map { manga -> - toMigrationMangaScreenItems(manga) - } - // KMK <-- .map { manga -> manga - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.manga.title }) + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) .toImmutableList() } .collectLatest { list -> - mutableState.update { it.copy(titleList = list) } + // KMK --> + mutableState.update { state -> + val titleIds = list.map { it.id }.toSet() + val selection = state.selection.intersect(titleIds).toMutableSet() + updateSelectedPositions(list, selection) + state.copy( + titleList = list, + selection = selection, + ) + } + // KMK <-- } } } - // KMK --> - private fun toMigrationMangaScreenItems(mangas: List): List { - return mangas.map { manga -> - MigrateMangaItem( - manga = manga, - selected = manga.id in selectedMangaIds, - ) - } - } - fun toggleSelection( - item: MigrateMangaItem, + item: Manga, + // KMK --> selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { mutableState.update { state -> - val newItems = state.titles.toMutableList().apply { - val selectedIndex = indexOfFirst { it.manga.id == item.manga.id } - if (selectedIndex < 0) return@apply + if (item.id in state.selection == selected) return@update state + val selectedIndex = state.titles.indexOfFirst { it.id == item.id } + if (selectedIndex < 0) return@update state - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply + val selection = state.selection.mutate { list -> + state.titles.run { + val firstSelection = list.isEmpty() + if (selected) list.add(item.id) else list.remove(item.id) - val firstSelection = none { it.selected } - set(selectedIndex, selectedItem.copy(selected = selected)) - selectedMangaIds.addOrRemove(item.manga.id, selected) - - if (selected && userSelected && fromLongPress) { - if (firstSelection) { - selectedPositions[0] = selectedIndex - selectedPositions[1] = selectedIndex - } else { - // Try to select the items in-between when possible - val range: IntRange - if (selectedIndex < selectedPositions[0]) { - range = selectedIndex + 1 until selectedPositions[0] + if (selected && fromLongPress) { + if (firstSelection) { selectedPositions[0] = selectedIndex - } else if (selectedIndex > selectedPositions[1]) { - range = (selectedPositions[1] + 1) until selectedIndex selectedPositions[1] = selectedIndex } else { - // Just select itself - range = IntRange.EMPTY - } + // Try to select the items in-between when possible + val range: IntRange + if (selectedIndex < selectedPositions[0]) { + range = selectedIndex + 1 until selectedPositions[0] + selectedPositions[0] = selectedIndex + } else if (selectedIndex > selectedPositions[1]) { + range = (selectedPositions[1] + 1) until selectedIndex + selectedPositions[1] = selectedIndex + } else { + // Just select itself + range = IntRange.EMPTY + } - range.forEach { - val inBetweenItem = get(it) - if (!inBetweenItem.selected) { - selectedMangaIds.add(inBetweenItem.manga.id) - set(it, inBetweenItem.copy(selected = true)) + range.forEach { + val inBetweenItem = get(it) + if (inBetweenItem.id !in list) { + list.add(inBetweenItem.id) + } } } - } - } else if (userSelected && !fromLongPress) { - if (!selected) { - if (selectedIndex == selectedPositions[0]) { - selectedPositions[0] = indexOfFirst { it.selected } - } else if (selectedIndex == selectedPositions[1]) { - selectedPositions[1] = indexOfLast { it.selected } - } - } else { - if (selectedIndex < selectedPositions[0]) { - selectedPositions[0] = selectedIndex - } else if (selectedIndex > selectedPositions[1]) { - selectedPositions[1] = selectedIndex + } else if (!fromLongPress) { + if (!selected) { + if (selectedIndex == selectedPositions[0]) { + selectedPositions[0] = indexOfFirst { it.id in list } + } else if (selectedIndex == selectedPositions[1]) { + selectedPositions[1] = indexOfLast { it.id in list } + } + } else { + if (selectedIndex < selectedPositions[0]) { + selectedPositions[0] = selectedIndex + } else if (selectedIndex > selectedPositions[1]) { + selectedPositions[1] = selectedIndex + } } } } } - state.copy(titleList = newItems.toImmutableList()) + // KMK <-- + state.copy(selection = selection) } } + // KMK --> + private fun updateSelectedPositions(titles: List, selection: Set) { + if (selection.isEmpty()) { + selectedPositions[0] = -1 + selectedPositions[1] = -1 + return + } + + selectedPositions[0] = titles.indexOfFirst { it.id in selection } + selectedPositions[1] = titles.indexOfLast { it.id in selection } + } + fun toggleAllSelection(selected: Boolean = true) { mutableState.update { state -> - val newItems = state.titles.map { - selectedMangaIds.addOrRemove(it.manga.id, selected) - it.copy(selected = selected) + val selection = if (selected) { + state.titles.mapTo(mutableSetOf()) { it.id } + } else { + emptySet() } selectedPositions[0] = -1 selectedPositions[1] = -1 - state.copy(titleList = newItems.toImmutableList()) + state.copy(selection = selection) } } fun invertSelection() { mutableState.update { state -> - val newItems = state.titles.map { - selectedMangaIds.addOrRemove(it.manga.id, !it.selected) - it.copy(selected = !it.selected) + val selection = state.selection.mutate { list -> + state.titles.forEach { item -> + if (!list.remove(item.id)) list.add(item.id) + } } selectedPositions[0] = -1 selectedPositions[1] = -1 - state.copy(titleList = newItems.toImmutableList()) + state.copy(selection = selection) } } // KMK <-- @@ -177,13 +189,11 @@ class MigrateMangaScreenModel( @Immutable data class State( val source: Source? = null, - private val titleList: ImmutableList? = null, + val selection: Set = emptySet(), + private val titleList: ImmutableList? = null, ) { - // KMK --> - val selection = titles.filter { it.selected } - // KMK <-- - val titles: ImmutableList + val titles: ImmutableList get() = titleList ?: persistentListOf() val isLoading: Boolean @@ -199,11 +209,3 @@ class MigrateMangaScreenModel( sealed interface MigrationMangaEvent { data object FailedFetchingFavorites : MigrationMangaEvent } - -// KMK --> -@Immutable -data class MigrateMangaItem( - val manga: Manga, - val selected: Boolean, -) -// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index cc875e266..edb69c9df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -375,6 +375,7 @@ data class BrowseSourceScreen( }, onMangaLongClick = { manga -> // KMK --> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) if (bulkFavoriteState.selectionMode) { navigator.push(MangaScreen(manga.id, true)) } else { @@ -388,7 +389,6 @@ data class BrowseSourceScreen( ) else -> screenModel.addFavorite(manga) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } } }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 4fb192350..34e6d80d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -298,14 +298,14 @@ class HistoryScreenModel( selectionOptions: HistorySelectionOptions, ) { val (selected, fromLongPress) = selectionOptions - if (item.chapterId in state.value.selection == selected) return mutableState.update { state -> + if (item.chapterId in state.selection == selected) return@update state + val selectedIndex = state.list.indexOfFirst { it.chapterId == item.chapterId } + if (selectedIndex < 0) return@update state + val selection = state.selection.mutate { list -> state.list.run { - val selectedIndex = indexOfFirst { it.chapterId == item.chapterId } - if (selectedIndex < 0) return@run - val firstSelection = list.isEmpty() if (selected) list.add(item.chapterId) else list.remove(item.chapterId) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index e40fb734c..639be1c2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -94,6 +94,7 @@ import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category.Companion.UNCATEGORIZED_ID +import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter @@ -142,6 +143,7 @@ class LibraryScreenModel( private val getTracksPerManga: GetTracksPerManga = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), + private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), @@ -549,15 +551,6 @@ class LibraryScreenModel( val filterLewd = preferences.filterLewd // SY <-- - // KMK --> - // Pre-compute track mappings for better performance - val mangaTrackIds = if (!isNotLoggedInAnyTrack && !trackFiltersIsIgnored) { - trackMap.mapValues { entry -> entry.value.map { it.trackerId } } - } else { - emptyMap() - } - // KMK <-- - val filterFnDownloaded: suspend (LibraryItem) -> Boolean = { applyFilter(filterDownloaded) { it.libraryManga.manga.isLocal() || @@ -609,15 +602,12 @@ class LibraryScreenModel( if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true // KMK --> - val mangaTracks = mangaTrackIds[item.id].orEmpty() + val mangaTracks = trackMap[item.id].orEmpty() - // Early return - if (mangaTracks.isEmpty() && includedTracks.isNotEmpty()) return@tracking false + val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it.trackerId in excludedTracks } + val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it.trackerId in includedTracks } // KMK <-- - val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks } - val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks } - !isExcluded && isIncluded } @@ -1019,15 +1009,19 @@ class LibraryScreenModel( * Queues the amount specified of unread chapters from the list of selected manga */ fun performDownloadAction(action: DownloadAction) { - val mangas = state.value.selectedManga - val amount = when (action) { - DownloadAction.NEXT_1_CHAPTER -> 1 - DownloadAction.NEXT_5_CHAPTERS -> 5 - DownloadAction.NEXT_10_CHAPTERS -> 10 - DownloadAction.NEXT_25_CHAPTERS -> 25 - DownloadAction.UNREAD_CHAPTERS -> null + when (action) { + DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1) + DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5) + DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10) + DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25) + DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null) + DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters() } clearSelection() + } + + private fun downloadNextChapters(amount: Int?) { + val mangas = state.value.selectedManga screenModelScope.launchNonCancellable { mangas.forEach { manga -> // SY --> @@ -1077,6 +1071,54 @@ class LibraryScreenModel( } } + private fun downloadBookmarkedChapters() { + val mangas = state.value.selectedManga + screenModelScope.launchNonCancellable { + mangas.forEach { manga -> + // SY --> + if (manga.source == MERGED_SOURCE_ID) { + val mergedMangas = getMergedMangaById.await(manga.id) + .associateBy { it.id } + getBookmarkedChaptersByMangaId.await(manga.id) + .groupBy { it.mangaId } + .forEach ab@{ (mangaId, chapters) -> + val mergedManga = mergedMangas[mangaId] ?: return@ab + val downloadChapters = chapters.fastFilterNot { chapter -> + downloadManager.queueState.value.fastAny { chapter.id == it.chapter.id } || + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + chapter.url, + mergedManga.ogTitle, + mergedManga.source, + ) + } + + downloadManager.downloadChapters(mergedManga, downloadChapters) + } + + return@forEach + } + // SY <-- + + val chapters = getBookmarkedChaptersByMangaId.await(manga.id) + .fastFilterNot { chapter -> + downloadManager.getQueuedDownloadOrNull(chapter.id) != null || + downloadManager.isChapterDownloaded( + chapter.name, + chapter.scanlator, + chapter.url, + // SY --> + manga.ogTitle, + // SY <-- + manga.source, + ) + } + downloadManager.downloadChapters(manga, chapters) + } + } + } + // SY --> fun cleanTitles() { val regex1 = "\\[.*?]".toRegex() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 5eda7ac7e..c35c63142 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -329,8 +329,8 @@ data object LibraryTab : Tab { }.takeIf { state.showMangaContinueButton }, onToggleSelection = screenModel::toggleSelection, onToggleRangeSelection = { category, manga -> - screenModel.toggleRangeSelection(category, manga) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + screenModel.toggleRangeSelection(category, manga) }, onRefresh = { onClickRefresh(state.activeCategory) }, onGlobalSearchClicked = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt index b63cf8ab7..85068c6ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/libraryUpdateError/LibraryUpdateErrorScreenModel.kt @@ -62,22 +62,20 @@ class LibraryUpdateErrorScreenModel( fun toggleSelection( item: LibraryUpdateErrorItem, selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { mutableState.update { state -> + val selectedIndex = state.items.indexOfFirst { it.error.errorId == item.error.errorId } + if (selectedIndex < 0) return@update state + val selectedItem = state.items[selectedIndex] + if (selectedItem.selected == selected) return@update state + val newItems = state.items.toMutableList().apply { - val selectedIndex = indexOfFirst { it.error.errorId == item.error.errorId } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedErrorIds.addOrRemove(item.error.errorId, selected) - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -96,14 +94,14 @@ class LibraryUpdateErrorScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedErrorIds.add(inbetweenItem.error.errorId) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedErrorIds.add(inBetweenItem.error.errorId) + set(it, inBetweenItem.copy(selected = true)) } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } 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 0a0f721f0..43bcfc49d 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 @@ -66,7 +66,7 @@ import eu.kanade.presentation.components.UpdatingBannerBackgroundColor import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog import eu.kanade.presentation.more.settings.screen.about.AboutScreen.Companion.getReleaseNotes import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.DefaultNavigatorScreenTransition @@ -693,11 +693,11 @@ class MainActivity : BaseActivity() { navigator.popUntilRoot() navigator.push(RestoreBackupScreen(intent.data.toString())) } - // Deep link to add extension repo - else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { + // Deep link to add extension store + else if (intent.isAddExtensionStoreIntent()) { intent.data?.getQueryParameter("url")?.let { repoUrl -> navigator.popUntilRoot() - navigator.push(ExtensionReposScreen(repoUrl)) + navigator.push(ExtensionStoresScreen(repoUrl)) } } null @@ -713,6 +713,11 @@ class MainActivity : BaseActivity() { return true } + private fun Intent.isAddExtensionStoreIntent(): Boolean { + return (scheme == "tachiyomi" && data?.host == "add-repo") || + (scheme == "mihon" && data?.host == "extension-store") + } + companion object { const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" const val INTENT_SEARCH_QUERY = "query" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index c2e296d82..9b3739ae3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -278,8 +278,8 @@ class MangaScreen( onChapterClicked = { openChapter(context, it) }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onAddToLibraryClicked = { - screenModel.toggleFavorite() haptic.performHapticFeedback(HapticFeedbackType.LongPress) + screenModel.toggleFavorite() }, // SY --> onWebViewClicked = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index e0ad1faf4..a17e2f39f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -555,8 +555,8 @@ class MangaScreenModel( } val vibrantColor = it.getBestColor() ?: return@launchIO mangaCover.vibrantCoverColor = vibrantColor - updateSuccessState { - it.copy(seedColor = Color(vibrantColor)) + updateSuccessState { state -> + state.copy(seedColor = Color(vibrantColor)) } } } @@ -1322,6 +1322,13 @@ class MangaScreenModel( return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted } + private fun getBookmarkedChapters(): List { + val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty() + return chapterItems + .filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED } + .map { it.chapter } + } + private fun startDownload( chapters: List, startNow: Boolean, @@ -1384,6 +1391,7 @@ class MangaScreenModel( DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10) DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25) DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters() + DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters() } if (chaptersToDownload.isNotEmpty()) { startDownload(chaptersToDownload, false) @@ -1725,22 +1733,22 @@ class MangaScreenModel( fun toggleSelection( item: ChapterList.Item, selected: Boolean, - userSelected: Boolean = false, fromLongPress: Boolean = false, ) { updateSuccessState { successState -> + // KMK --> + val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } + if (selectedIndex < 0) return@updateSuccessState successState + val selectedItem = successState.processedChapters[selectedIndex] + if (selectedItem.selected == selected) return@updateSuccessState successState + // KMK <-- + val newChapters = successState.processedChapters.toMutableList().apply { - val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if ((selectedItem.selected && selected) || (!selectedItem.selected && !selected)) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedChapterIds.addOrRemove(item.id, selected) - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -1759,14 +1767,14 @@ class MangaScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.id) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedChapterIds.add(inBetweenItem.id) + set(it, inBetweenItem.copy(selected = true)) } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt index 0740598c4..27c09161b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt @@ -74,15 +74,8 @@ class PagerViewerAdapter( val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0 val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0 - // Add previous chapter pages and transition. - if (chapters.prevChapter != null) { - // We only need to add the last few pages of the previous chapter, because it'll be - // selected as the current chapter when one of those pages is selected. - val prevPages = chapters.prevChapter.pages - if (prevPages != null) { - newItems.addAll(prevPages.takeLast(2)) - } - } + // Add previous chapter pages and transition + chapters.prevChapter?.pages?.let(newItems::addAll) // Skip transition page if the chapter is loaded & current page is not a transition page if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { @@ -124,14 +117,7 @@ class PagerViewerAdapter( } } - if (chapters.nextChapter != null) { - // Add at most two pages, because this chapter will be selected before the user can - // swap more pages. - val nextPages = chapters.nextChapter.pages - if (nextPages != null) { - newItems.addAll(nextPages.take(2)) - } - } + chapters.nextChapter?.pages?.let(newItems::addAll) // Resets double-page splits, else insert pages get misplaced subItems.filterIsInstance().also { subItems.removeAll(it) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index 5d0ddfc60..799d1a4bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -49,14 +49,7 @@ class WebtoonAdapter( val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0 // Add previous chapter pages and transition. - if (chapters.prevChapter != null) { - // We only need to add the last few pages of the previous chapter, because it'll be - // selected as the current chapter when one of those pages is selected. - val prevPages = chapters.prevChapter.pages - if (prevPages != null) { - newItems.addAll(prevPages.takeLast(2)) - } - } + chapters.prevChapter?.pages?.let(newItems::addAll) // Skip transition page if the chapter is loaded & current page is not a transition page if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) { @@ -76,14 +69,7 @@ class WebtoonAdapter( newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)) } - if (chapters.nextChapter != null) { - // Add at most two pages, because this chapter will be selected before the user can - // swap more pages. - val nextPages = chapters.nextChapter.pages - if (nextPages != null) { - newItems.addAll(nextPages.take(2)) - } - } + chapters.nextChapter?.pages?.let(newItems::addAll) updateItems(newItems) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 5fbaba32d..9aa4d5aa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -362,7 +362,6 @@ class UpdatesScreenModel( /** Bundles all of the boolean flags for update‐selection into one type */ data class UpdateSelectionOptions( val selected: Boolean, - val userSelected: Boolean = false, val fromLongPress: Boolean = false, val isGroup: Boolean = false, val isExpanded: Boolean = false, @@ -376,16 +375,17 @@ class UpdatesScreenModel( // KMK <-- ) { // KMK --> - val (selected, userSelected, fromLongPress, isGroup, isExpanded) = selectionOptions + val (selected, fromLongPress, isGroup, isExpanded) = selectionOptions // KMK <-- mutableState.update { state -> + // KMK --> + val selectedIndex = state.items.indexOfFirst { it.update.chapterId == item.update.chapterId } + if (selectedIndex < 0) return@update state + val selectedItem = state.items[selectedIndex] + if (selectedItem.selected == selected) return@update state + // KMK <-- + val newItems = state.items.toMutableList().apply { - val selectedIndex = indexOfFirst { it.update.chapterId == item.update.chapterId } - if (selectedIndex < 0) return@apply - - val selectedItem = get(selectedIndex) - if (selectedItem.selected == selected) return@apply - val firstSelection = none { it.selected } set(selectedIndex, selectedItem.copy(selected = selected)) selectedChapterIds.addOrRemove(item.update.chapterId, selected) @@ -409,7 +409,7 @@ class UpdatesScreenModel( } // KMK <-- - if (selected && userSelected && fromLongPress) { + if (selected && fromLongPress) { if (firstSelection) { selectedPositions[0] = selectedIndex selectedPositions[1] = selectedIndex @@ -428,14 +428,14 @@ class UpdatesScreenModel( } range.forEach { - val inbetweenItem = get(it) - if (!inbetweenItem.selected) { - selectedChapterIds.add(inbetweenItem.update.chapterId) - set(it, inbetweenItem.copy(selected = true)) + val inBetweenItem = get(it) + if (!inBetweenItem.selected) { + selectedChapterIds.add(inBetweenItem.update.chapterId) + set(it, inBetweenItem.copy(selected = true)) } } } - } else if (userSelected && !fromLongPress) { + } else if (!fromLongPress) { if (!selected) { if (selectedIndex == selectedPositions[0]) { selectedPositions[0] = indexOfFirst { it.selected } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index 4ac770bbb..1d5ddeff1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -67,7 +67,7 @@ class CrashLogUtil( """ - ${it.name} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} - Obsolete: ${it.isObsolete} + Orphaned: ${it.isObsolete} """.trimIndent() } diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index ffe00517a..2a662129c 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -177,7 +177,7 @@ object EXHMigrations { out += "#" + uri.fragment } out - } catch (e: URISyntaxException) { + } catch (_: URISyntaxException) { orig } } diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt index 2d0b30c53..b0cdba977 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt @@ -40,6 +40,7 @@ import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING +import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_NETWORK_NOT_METERED import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata import tachiyomi.domain.manga.interactor.GetFlatMetadataById @@ -289,17 +290,27 @@ class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerPara val interval = prefInterval ?: exhPreferences.exhAutoUpdateFrequency().get() if (interval > 0) { val restrictions = prefRestrictions ?: exhPreferences.exhAutoUpdateRequirements().get() - val acRestriction = DEVICE_CHARGING in restrictions - - val networkRequestBuilder = NetworkRequest.Builder() - if (DEVICE_ONLY_ON_WIFI in restrictions) { - networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { + NetworkType.UNMETERED + } else { + NetworkType.CONNECTED } + val networkRequest = NetworkRequest.Builder().apply { + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + if (DEVICE_ONLY_ON_WIFI in restrictions) { + addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + } + if (DEVICE_NETWORK_NOT_METERED in restrictions) { + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } + } + .build() val constraints = Constraints.Builder() // 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used - .setRequiredNetworkRequest(networkRequestBuilder.build(), NetworkType.CONNECTED) - .setRequiresCharging(acRestriction) + .setRequiredNetworkRequest(networkRequest, networkType) + .setRequiresCharging(DEVICE_CHARGING in restrictions) + .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder( diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index 71b907bb6..658823b05 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -127,6 +127,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { }, onMangaLongClick = { manga -> // KMK --> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) if (bulkFavoriteState.selectionMode) { navigator.push(MangaScreen(manga.id, true)) } else { @@ -140,7 +141,6 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { ) else -> screenModel.addFavorite(manga) } - haptic.performHapticFeedback(HapticFeedbackType.LongPress) } } }, diff --git a/app/src/main/java/exh/source/SourceHelper.kt b/app/src/main/java/exh/source/SourceHelper.kt index ca5c6f5b6..ea279e8a6 100644 --- a/app/src/main/java/exh/source/SourceHelper.kt +++ b/app/src/main/java/exh/source/SourceHelper.kt @@ -5,9 +5,7 @@ import eu.kanade.tachiyomi.source.online.all.Lanraragi import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.english.EightMuses -import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.Pururin -import eu.kanade.tachiyomi.source.online.english.Tsumino /** * Source helpers @@ -16,8 +14,6 @@ import eu.kanade.tachiyomi.source.online.english.Tsumino private val DELEGATED_METADATA_SOURCES by lazy { listOf( Pururin::class, - Tsumino::class, - HBrowse::class, EightMuses::class, NHentai::class, Lanraragi::class, diff --git a/app/src/main/java/exh/ui/login/EhLoginActivity.kt b/app/src/main/java/exh/ui/login/EhLoginActivity.kt index d1568e8d5..10c5191fc 100644 --- a/app/src/main/java/exh/ui/login/EhLoginActivity.kt +++ b/app/src/main/java/exh/ui/login/EhLoginActivity.kt @@ -93,14 +93,30 @@ class EhLoginActivity : BaseActivity() { xLogD(url) val parsedUrl = url.toUri() if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) { - // Hide distracting content - if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) { - view.evaluateJavascript(HIDE_JS, null) - } - // Check login result + view.evaluateJavascript( + """ + (function() { + let html = document.documentElement.innerHTML; + return html.includes("/cdn-cgi/"); + })(); + """.trimIndent(), + ) { result -> + val isCloudflareBlock = result == "true" - if (parsedUrl.getQueryParameter("code")?.toInt() != 0) { - if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/") + if (isCloudflareBlock) { + xLogD("Cloudflare block detected — skipping logic") + return@evaluateJavascript + } + + // Hide distracting content + if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) { + view.evaluateJavascript(HIDE_JS, null) + } + // Check login result + + if (parsedUrl.getQueryParameter("code")?.toIntOrNull() != 0) { + if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/") + } } } else if (parsedUrl.host.equals("exhentai.org", ignoreCase = true)) { // At ExHentai, check that everything worked out... diff --git a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt deleted file mode 100644 index 1726f5c67..000000000 --- a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package exh.ui.metadata.adapters - -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.DescriptionAdapterHbBinding -import eu.kanade.tachiyomi.ui.manga.MangaScreenModel.State -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.dpToPx -import exh.metadata.metadata.HBrowseSearchMetadata -import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable -import tachiyomi.core.common.i18n.pluralStringResource -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.sy.SYMR - -@Composable -fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) { - val context = LocalContext.current - // KMK --> - val colorScheme = AndroidViewColorScheme(MaterialTheme.colorScheme) - val iconColor = colorScheme.iconColor - val textColor = LocalContentColor.current.toArgb() - // KMK <-- - AndroidView( - modifier = Modifier.fillMaxWidth(), - factory = { factoryContext -> - DescriptionAdapterHbBinding.inflate(LayoutInflater.from(factoryContext)).root - }, - update = { - val meta = state.meta - if (meta == null || meta !is HBrowseSearchMetadata) return@AndroidView - val binding = DescriptionAdapterHbBinding.bind(it) - - binding.pages.text = - context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) - // KMK --> - binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor, 4.dpToPx) - binding.pages.setTextColor(textColor) - - binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor) - binding.moreInfo.text = context.stringResource(SYMR.strings.more_info) - binding.moreInfo.setTextColor(iconColor) - // KMK <-- - - binding.pages.setOnLongClickListener { - context.copyToClipboard( - binding.pages.text.toString(), - binding.pages.text.toString(), - ) - true - } - - binding.moreInfo.setOnClickListener { - openMetadataViewer() - } - }, - ) -} diff --git a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt deleted file mode 100644 index ca2eed78d..000000000 --- a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package exh.ui.metadata.adapters - -import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.view.LayoutInflater -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.DescriptionAdapterTsBinding -import eu.kanade.tachiyomi.ui.manga.MangaScreenModel.State -import eu.kanade.tachiyomi.util.system.copyToClipboard -import eu.kanade.tachiyomi.util.system.dpToPx -import exh.metadata.metadata.TsuminoSearchMetadata -import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable -import exh.util.SourceTagsUtil.genreTextColor -import tachiyomi.core.common.i18n.pluralStringResource -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import java.util.Date -import kotlin.math.round - -@Composable -fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) { - val context = LocalContext.current - // KMK --> - val colorScheme = AndroidViewColorScheme(MaterialTheme.colorScheme) - val iconColor = colorScheme.iconColor - val ratingBarColor = colorScheme.ratingBarColor - val ratingBarSecondaryColor = colorScheme.ratingBarSecondaryColor - val textColor = LocalContentColor.current.toArgb() - // KMK <-- - AndroidView( - modifier = Modifier.fillMaxWidth(), - factory = { factoryContext -> - DescriptionAdapterTsBinding.inflate(LayoutInflater.from(factoryContext)).root - }, - update = { - val meta = state.meta - if (meta == null || meta !is TsuminoSearchMetadata) return@AndroidView - val binding = DescriptionAdapterTsBinding.bind(it) - - binding.genre.text = meta.category?.let { genre -> MetadataUIUtil.getGenreAndColour(context, genre) } - ?.let { (genre, name) -> - binding.genre.setBackgroundColor(genre.color) - // KMK --> - binding.genre.setTextColor(genreTextColor(genre)) - // KMK <-- - name - } ?: meta.category ?: context.stringResource(MR.strings.unknown) - - binding.favorites.text = (meta.favorites ?: 0).toString() - // KMK --> - binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor) - binding.favorites.setTextColor(textColor) - // KMK <-- - - binding.whenPosted.text = TsuminoSearchMetadata.TSUMINO_DATE_FORMAT.format(Date(meta.uploadDate ?: 0)) - // KMK --> - binding.whenPosted.setTextColor(textColor) - // KMK <-- - - binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown) - // KMK --> - binding.uploader.setTextColor(textColor) - // KMK <-- - - binding.pages.text = - context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0) - // KMK --> - binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor, 4.dpToPx) - binding.pages.setTextColor(textColor) - // KMK <-- - - binding.ratingBar.rating = meta.averageRating ?: 0F - @SuppressLint("SetTextI18n") - binding.rating.text = - (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + - MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2)) - // KMK --> - binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(ratingBarColor) - binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor) - binding.rating.setTextColor(textColor) - - binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor) - binding.moreInfo.text = context.stringResource(SYMR.strings.more_info) - binding.moreInfo.setTextColor(iconColor) - // KMK <-- - - listOf( - binding.favorites, - binding.genre, - binding.pages, - binding.rating, - binding.uploader, - binding.whenPosted, - ).forEach { textView -> - textView.setOnLongClickListener { - context.copyToClipboard( - textView.text.toString(), - textView.text.toString(), - ) - true - } - } - - binding.moreInfo.setOnClickListener { - openMetadataViewer() - } - }, - ) -} diff --git a/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt b/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt deleted file mode 100644 index 2fd77047b..000000000 --- a/app/src/main/java/mihon/core/migration/migrations/DelegateHBrowseMigration.kt +++ /dev/null @@ -1,29 +0,0 @@ -package mihon.core.migration.migrations - -import eu.kanade.domain.manga.interactor.UpdateManga -import exh.source.HBROWSE_OLD_ID -import exh.source.HBROWSE_SOURCE_ID -import mihon.core.migration.MigrateUtils -import mihon.core.migration.Migration -import mihon.core.migration.MigrationContext -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.domain.manga.interactor.GetMangaBySource -import tachiyomi.domain.manga.model.MangaUpdate - -class DelegateHBrowseMigration : Migration { - override val version: Float = 4f - - override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { - val getMangaBySource = migrationContext.get() ?: return@withIOContext false - val updateManga = migrationContext.get() ?: return@withIOContext false - MigrateUtils.updateSourceId(migrationContext, HBROWSE_SOURCE_ID, HBROWSE_OLD_ID) - - // Migrate BHrowse URLs - val hBrowseManga = getMangaBySource.await(HBROWSE_SOURCE_ID) - val mangaUpdates = hBrowseManga.map { - MangaUpdate(it.id, url = it.url + "/c00001/") - } - updateManga.awaitAll(mangaUpdates) - return@withIOContext true - } -} diff --git a/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt new file mode 100644 index 000000000..210db5d4b --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt @@ -0,0 +1,30 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import eu.kanade.domain.source.service.SourcePreferences +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import tachiyomi.core.common.util.lang.withIOContext + +class DisabledRepoMigration : Migration { + override val version: Float = 80f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { + val context = migrationContext.get() ?: return@withIOContext false + val sourcePreferences = migrationContext.get() ?: return@withIOContext false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.edit { + val disabledRepos = prefs.getStringSet(sourcePreferences.disabledRepos().key(), emptySet()) ?: return@edit + disabledRepos + .map { + it.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json" + }.toSet() + .let { + putStringSet(sourcePreferences.disabledRepos().key(), it) + } + } + return@withIOContext true + } +} diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index 3726896cc..ccb642fca 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -8,7 +8,6 @@ val migrations: List SetupLibraryUpdateMigration(), SetupEHentaiUpdateMigration(), SetupSyncDataMigration(), - // DelegateHBrowseMigration(), // DelegateNHentaiMigration(), // MergedMangaRewriteMigration(), // LogoutFromMALMigration(), @@ -53,4 +52,8 @@ val migrations: List TrustExtensionRepositoryMigration(), CategoryPreferencesCleanupMigration(), RemoveDuplicateReaderPreferenceMigration(), + // KMK --> + DisabledRepoMigration(), + SyncPrefKeyMigration(), + // KMK <-- ) diff --git a/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt b/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt new file mode 100644 index 000000000..1cdfc7b26 --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/SyncPrefKeyMigration.kt @@ -0,0 +1,70 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import tachiyomi.core.common.preference.Preference + +/** + * Migrates sync preference storage keys to namespaced forms so that + * connection keys ("connection_*") and sync-toggle keys ("sync_*") no longer + * collide with unrelated preferences. + */ +class SyncPrefKeyMigration : Migration { + override val version: Float = 80f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean { + val context = migrationContext.get() ?: return false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val stringRenames = listOf( + // Sync service connection + "sync_client_host" to "connection_sync_client_host", + "sync_client_api_key" to "connection_sync_client_api_key", + // WebDAV connection (KMK) + "webdav_url" to "connection_webdav_url", + "webdav_username" to "connection_webdav_username", + "webdav_password" to "connection_webdav_password", + "webdav_folder" to "connection_webdav_folder", + // Google Drive tokens — appStateKey() prepends "__APP_STATE_" + Preference.appStateKey("google_drive_access_token") to Preference.appStateKey("connection_google_drive_access_token"), + Preference.appStateKey("google_drive_refresh_token") to Preference.appStateKey("connection_google_drive_refresh_token"), + ) + + val booleanRenames = listOf( + // Core sync toggles + "library_entries" to "sync_library_entries", + "categories" to "sync_categories", + "chapters" to "sync_chapters", + "tracking" to "sync_tracking", + "history" to "sync_history", + "appSettings" to "sync_appSettings", + "extensionRepoSettings" to "sync_extensionStores", + "sourceSettings" to "sync_sourceSettings", + "privateSettings" to "sync_privateSettings", + // SY sync toggles + "customInfo" to "sync_customInfo", + "readEntries" to "sync_readEntries", + "savedSearchesFeeds" to "sync_savedSearchesFeeds", + ) + + prefs.edit { + stringRenames.forEach { (oldKey, newKey) -> + if (prefs.contains(oldKey)) { + putString(newKey, prefs.getString(oldKey, null)) + remove(oldKey) + } + } + booleanRenames.forEach { (oldKey, newKey) -> + if (prefs.contains(oldKey)) { + putBoolean(newKey, prefs.getBoolean(oldKey, true)) + remove(oldKey) + } + } + } + + return true + } +} diff --git a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt index b5dd80ff5..1ef23929a 100644 --- a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt @@ -4,8 +4,7 @@ import eu.kanade.domain.source.service.SourcePreferences import logcat.LogPriority import mihon.core.migration.Migration import mihon.core.migration.MigrationContext -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -14,18 +13,14 @@ class TrustExtensionRepositoryMigration : Migration { override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { val sourcePreferences = migrationContext.get() ?: return@withIOContext false - val extensionRepositoryRepository = - migrationContext.get() ?: return@withIOContext false + val repository = migrationContext.get() ?: return@withIOContext false for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) { try { - extensionRepositoryRepository.upsertRepo( - source, - "Repo #${index + 1}", - null, - source, - "NOFINGERPRINT-${index + 1}", + repository.insertFromPreference( + indexUrl = source.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json", + name = "Repo #${index + 1}", ) - } catch (e: SaveExtensionRepoException) { + } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } } } diff --git a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt index 4de4a5fc8..a17709104 100644 --- a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -99,6 +99,7 @@ class MigrateMangaUseCase( // SY <-- dateFetch = prevChapter.dateFetch, bookmark = prevChapter.bookmark, + lastPageRead = prevChapter.lastPageRead, ) // SY --> // KMK --> diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index dcacae77a..c81ca04b6 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -426,7 +426,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { } } - private fun updateSources(save: Boolean = true, action: (List) -> List) { + private fun updateSources(action: (List) -> List) { mutableState.update { state -> val updatedSources = action(state.sources) val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id } @@ -435,7 +435,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { // KMK <-- state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources))) } - if (save) saveSources() + saveSources() } private fun initSources() { @@ -451,9 +451,6 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { .asSequence() .filterIsInstance() .filter { it.lang in languages } - // KMK --> - .sortedWith(compareBy { includedSources[it.id] ?: Int.MAX_VALUE }) - // KMK <-- .map { val source = Source( id = it.id, @@ -473,7 +470,9 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { } .toList() - updateSources(save = false) { sources } + mutableState.update { state -> + state.copy(sources = sources.sortedWith(sourcesComparator(includedSources))) + } } fun toggleSelection(id: Long) { diff --git a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt index 5086d056b..4a65dd0e8 100644 --- a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt +++ b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt @@ -154,12 +154,11 @@ private class MigrateDialogScreenModel( } val selectedFlags = sourcePreference.migrationFlags().get() mutableState.update { - it.copy( + State( current = current, target = target, applicableFlags = applicableFlags, selectedFlags = selectedFlags, - isMigrated = false, ) } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt new file mode 100644 index 000000000..55f4b00d6 --- /dev/null +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/HttpException.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.network + +import okhttp3.Response + +/** + * Exception that handles HTTP codes considered not successful by OkHttp. + * Use it to have a standardized error message in the app across the extensions. + * + * @see Response.isSuccessful + * @since tachiyomix 1.6 + * @param code [Int] the HTTP status code + */ +class HttpException(val code: Int) : IllegalStateException("HTTP error $code") diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 37dc1e480..090099820 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -82,8 +82,6 @@ import kotlin.random.Random } } - val nonCloudflareClient /* KMK --> */ by lazy /* KMK <-- */ { clientBuilder().build() } - /* SY --> */ open /* SY <-- */ val client /* KMK --> */ by lazy /* KMK <-- */ { clientBuilder() .addInterceptor( diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 0a860d069..fd7917278 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.network -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.Json @@ -23,6 +22,7 @@ import kotlin.coroutines.resumeWithException val jsonMime = "application/json; charset=utf-8".toMediaType() @OptIn(ExperimentalAtomicApi::class) +@Deprecated("Use suspend APIs instead") fun Call.asObservable(): Observable { return Observable.unsafeCreate { subscriber -> // Since Call is a one-shot type, clone it for each new subscriber. @@ -61,6 +61,7 @@ fun Call.asObservable(): Observable { } } +@Deprecated("Use suspend APIs instead") fun Call.asObservableSuccess(): Observable { return asObservable().doOnNext { response -> if (!response.isSuccessful) { @@ -70,35 +71,31 @@ fun Call.asObservableSuccess(): Observable { } } -// Based on https://github.com/gildor/kotlin-coroutines-okhttp -@OptIn(ExperimentalCoroutinesApi::class) +// Based on https://github.com/square/okhttp/blob/master/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt +// and https://github.com/gildor/kotlin-coroutines-okhttp private suspend fun Call.await(callStack: Array): Response { return suspendCancellableCoroutine { continuation -> - val callback = - object : Callback { - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) { - response.body.close() - } - } - - override fun onFailure(call: Call, e: IOException) { - // Don't bother with resuming the continuation if it is already cancelled. - if (continuation.isCancelled) return - val exception = IOException(e.message, e).apply { stackTrace = callStack } - continuation.resumeWithException(exception) - } - } - - enqueue(callback) - continuation.invokeOnCancellation { try { - cancel() - } catch (ex: Throwable) { - // Ignore cancel exception + this.cancel() + } catch (_: Throwable) { + // ignore } } + + this.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + val exception = IOException(e.message, e).apply { stackTrace = callStack } + continuation.resumeWithException(exception) + } + + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) { _, value, _ -> + value.close() + } + } + }) } } @@ -108,7 +105,7 @@ suspend fun Call.await(): Response { } /** - * @since extensions-lib 1.5 + * Similar to [await] but throws [HttpException] if [Response.isSuccessful] returns false */ suspend fun Call.awaitSuccess(): Response { val callStack = Exception().stackTrace.run { copyOfRange(1, size) } @@ -148,12 +145,3 @@ fun decodeFromJsonResponse( json.decodeFromBufferedSource(deserializer, it) } } - -/** - * Exception that handles HTTP codes considered not successful by OkHttp. - * Use it to have a standardized error message in the app across the extensions. - * - * @since extensions-lib 1.5 - * @param code [Int] the HTTP status code - */ -class HttpException(val code: Int) : IllegalStateException("HTTP error $code") diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 552bae9b3..9d44c54bf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -25,7 +25,7 @@ android { kotlin { compilerOptions { - freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi") + optIn.add("kotlinx.serialization.ExperimentalSerializationApi") } } @@ -34,5 +34,9 @@ dependencies { implementation(projects.domain) implementation(projects.core.common) + implementation(kotlinx.serialization.json) + implementation(kotlinx.serialization.json.okio) + implementation(kotlinx.serialization.protobuf) + api(libs.bundles.sqldelight) } diff --git a/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt new file mode 100644 index 000000000..fa04cfc75 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt @@ -0,0 +1,7 @@ +package mihon.data.extension.model + +import mihon.domain.extension.model.ExtensionStore + +interface BaseNetworkExtensionStore { + fun toExtensionStore(indexUrl: String): ExtensionStore +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt new file mode 100644 index 000000000..ce051ee2d --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt @@ -0,0 +1,114 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkExtensionStore( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val badgeLabel: String, + @ProtoNumber(3) val signingKey: String, + @ProtoNumber(4) val contact: Contact, + @ProtoNumber(5) val extensions: List, +) : BaseNetworkExtensionStore { + @Serializable + data class Contact( + @ProtoNumber(1) val website: String, + @ProtoNumber(2) val discord: String?, + ) + + @Serializable + data class Extension( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val packageName: String, + @ProtoNumber(3) val resources: Resources, + @ProtoNumber(4) val extensionLib: String, + @ProtoNumber(5) val versionCode: Long, + @ProtoNumber(6) val versionName: String, + @ProtoNumber(7) val sources: List, + ) + + @Serializable + data class Resources( + @ProtoNumber(1) val apkUrl: String, + @ProtoNumber(2) val iconUrl: String, + ) + + @Serializable + data class Source( + @ProtoNumber(1) val id: Long, + @ProtoNumber(2) val name: String, + @ProtoNumber(3) val language: String, + @ProtoNumber(4) val homeUrl: String = "", + @ProtoNumber(5) val mirrorUrls: List = emptyList(), + @ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE, + @ProtoNumber(7) val message: String? = null, + ) + + @Suppress("Unused") + enum class ContentRating { + @ProtoNumber(0) + @JsonNames("CONTENT_RATING_SAFE") + SAFE, + + @ProtoNumber(1) + @JsonNames("CONTENT_RATING_SUGGESTIVE") + SUGGESTIVE, + + @ProtoNumber(2) + @JsonNames("CONTENT_RATING_EROTICA") + EROTICA, + + @ProtoNumber(3) + @JsonNames("CONTENT_RATING_PORNOGRAPHIC") + PORNOGRAPHIC, + } + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contact.website, + discord = contact.discord, + ), + isLegacy = false, + ) + } + + fun toAvailableExtensions(store: ExtensionStore): List { + return extensions.map { extension -> + val lang = extension.sources.map { it.language }.toSet() + eu.kanade.tachiyomi.extension.model.Extension.Available( + name = extension.name, + pkgName = extension.packageName, + apkUrl = extension.resources.apkUrl, + iconUrl = extension.resources.iconUrl, + libVersion = extension.extensionLib.toDouble(), + versionCode = extension.versionCode, + versionName = extension.versionName, + lang = if (lang.size == 1) lang.first() else "all", + isNsfw = extension.sources.maxOfOrNull { it.contentRating } == ContentRating.PORNOGRAPHIC, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + sources = extension.sources.map { source -> + eu.kanade.tachiyomi.extension.model.Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.language, + baseUrl = source.homeUrl, + ) + }, + store = store, + ) + } + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt new file mode 100644 index 000000000..85d18b21a --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt @@ -0,0 +1,65 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.serialization.Serializable +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtension( + val name: String, + val pkg: String, + val apk: String, + val lang: String, + val code: Long, + val version: String, + val nsfw: Int, + val sources: List?, +) { + @Serializable + data class Source( + val id: Long, + val lang: String, + val name: String, + val baseUrl: String, + ) + + fun toAvailableExtension(store: ExtensionStore, storeBaseUrl: String): Extension.Available { + return Extension.Available( + name = name.substringAfter("Tachiyomi: "), + pkgName = pkg, + apkUrl = "$storeBaseUrl/apk/$apk", + iconUrl = "$storeBaseUrl/icon/$pkg.png", + libVersion = version.substringBeforeLast('.').toDouble(), + versionCode = code, + versionName = version, + lang = lang, + isNsfw = nsfw == 1, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + sources = if (sources.isNullOrEmpty()) { + listOf( + Extension.Available.Source( + id = 0, + name = name, + lang = lang, + baseUrl = "", + ), + ) + } else { + sources.map { source -> + Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.lang, + baseUrl = source.baseUrl, + ) + } + }, + store = store, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt new file mode 100644 index 000000000..babc696bb --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt @@ -0,0 +1,36 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtensionRepo( + @SerialName("index_v2") + val indexV2: String?, + val meta: Meta, +) : BaseNetworkExtensionStore { + @Serializable + data class Meta( + val name: String, + val shortName: String?, + val website: String, + val signingKeyFingerprint: String, + ) + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = meta.name, + badgeLabel = meta.shortName ?: meta.name, + signingKey = meta.signingKeyFingerprint, + contact = ExtensionStore.Contact( + website = meta.website, + discord = null, + ), + isLegacy = true, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt new file mode 100644 index 000000000..71bb959dd --- /dev/null +++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt @@ -0,0 +1,145 @@ +package mihon.data.extension.repository + +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.supervisorScope +import logcat.LogPriority +import mihon.data.extension.service.ExtensionStoreService +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository +import tachiyomi.core.common.util.system.logcat +import tachiyomi.data.DatabaseHandler + +class ExtensionStoreRepositoryImpl( + private val service: ExtensionStoreService, + private val handler: DatabaseHandler, +) : ExtensionStoreRepository { + override suspend fun insert(indexUrl: String): Result { + return service.fetch(indexUrl).mapCatching { upsert(it) } + } + + override suspend fun insertFromPreference(indexUrl: String, name: String) { + handler.await { + extension_storeQueries.upsert( + indexUrl = indexUrl, + name = name, + badgeLabel = name, + signingKey = "NO_SIGNING_KEY", + contactWebsite = indexUrl, + contactDiscord = null, + isLegacy = false, + ) + } + } + + override suspend fun refreshAll() { + try { + handler.awaitList { + extension_storeQueries.getAll() + }.forEach { store -> + service.fetch(store.index_url) + .mapCatching { + handler.await { + upsert(it) + if (store.index_url != it.indexUrl) { + extension_storeQueries.delete(store.index_url) + } + } + } + .onFailure { + logcat(LogPriority.ERROR, it) { + "Failed to refresh extension store '${store.name} (${store.index_url})'" + } + } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } + } + + private suspend fun upsert(store: ExtensionStore) { + handler.await { + extension_storeQueries.upsert( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) + } + } + + override suspend fun fetchExtensions( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): List { + return try { + supervisorScope { + handler.awaitList { + extension_storeQueries.getAll(::extensionStoreMapper) + } + // KMK --> + .filterNot { it.indexUrl in disabledRepos } + // KMK <-- + .map { store -> + async { + service.getExtensions(store).onFailure { + this@ExtensionStoreRepositoryImpl.logcat(LogPriority.ERROR, it) { + "Failed to fetch extensions for store '${store.name} (${store.indexUrl})'" + } + } + } + } + .awaitAll() + .flatMap { it.getOrDefault(emptyList()) } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } + + override suspend fun getAll(): List { + return handler.awaitList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getAllAsFlow(): Flow> { + return handler.subscribeToList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getCountAsFlow(): Flow { + return handler.subscribeToOne { + extension_storeQueries + .getCount() + } + } + + override suspend fun remove(indexUrl: String) { + handler.await { extension_storeQueries.delete(indexUrl) } + } + + private fun extensionStoreMapper( + indexUrl: String, + name: String, + badgeLabel: String, + signingKey: String, + contactWebsite: String, + contactDiscord: String?, + isLegacy: Boolean, + ): ExtensionStore = ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contactWebsite, + discord = contactDiscord, + ), + isLegacy = isLegacy, + ) +} diff --git a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt new file mode 100644 index 000000000..de08e9bf4 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt @@ -0,0 +1,117 @@ +package mihon.data.extension.service + +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.awaitSuccess +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.okio.decodeFromBufferedSource +import kotlinx.serialization.protobuf.ProtoBuf +import logcat.LogPriority +import mihon.data.extension.model.NetworkExtensionStore +import mihon.data.extension.model.NetworkLegacyExtension +import mihon.data.extension.model.NetworkLegacyExtensionRepo +import mihon.domain.extension.model.ExtensionStore +import tachiyomi.core.common.util.system.logcat +import kotlin.coroutines.cancellation.CancellationException + +class ExtensionStoreService( + private val network: NetworkHelper, + private val json: Json, + private val protoBuf: ProtoBuf, +) { + suspend fun fetch(indexUrl: String): Result { + return fetch(indexUrl, forceV2 = false) + } + + private suspend fun fetch(indexUrl: String, forceV2: Boolean): Result { + var updatedIndexUrl: String = indexUrl + return try { + val store = network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source -> + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + try { + json.decodeFromBufferedSource(source.peek()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + if (forceV2) throw e + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + val legacyIndex = try { + json.decodeFromBufferedSource(source.peek()) + } catch (e: IllegalArgumentException) { + if (!indexUrl.endsWith("/index.min.json")) { + throw e + } + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json") + network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().use { + json.decodeFromBufferedSource(it.body.source()) + } + } + + if (legacyIndex.indexV2 != null) { + return fetch(legacyIndex.indexV2, forceV2 = true) + } else { + legacyIndex + } + } + } + .toExtensionStore(updatedIndexUrl) + } + Result.success(store) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + Result.failure(e) + } + } + + suspend fun getExtensions(store: ExtensionStore): Result> { + return try { + val extensions = if (!store.isLegacy) { + network.client.newCall(GET(store.indexUrl)).awaitSuccess().use { response -> + val source = response.body.source() + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + .toAvailableExtensions(store) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + json.decodeFromBufferedSource(source.peek()) + .toAvailableExtensions(store) + } + } + } else { + val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json") + network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess().use { response -> + val source = response.body.source() + json.decodeFromBufferedSource>(source) + .map { it.toAvailableExtension(store, storeBaseUrl) } + } + } + Result.success(extensions) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt deleted file mode 100644 index 24910d968..000000000 --- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt +++ /dev/null @@ -1,93 +0,0 @@ -package mihon.data.repository - -import android.database.sqlite.SQLiteException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import tachiyomi.data.DatabaseHandler - -class ExtensionRepoRepositoryImpl( - private val handler: DatabaseHandler, -) : ExtensionRepoRepository { - override fun subscribeAll(): Flow> { - return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) } - } - - override suspend fun getAll(): List { - return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) } - } - - override suspend fun getRepo(baseUrl: String): ExtensionRepo? { - return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } - } - - override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { - return handler.awaitOneOrNull { - extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) - } - } - - override fun getCount(): Flow { - return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } - } - - override suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) } - } catch (ex: SQLiteException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) } - } catch (ex: SQLiteException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun replaceRepo(newRepo: ExtensionRepo) { - handler.await { - extension_reposQueries.replace( - newRepo.baseUrl, - newRepo.name, - newRepo.shortName, - newRepo.website, - newRepo.signingKeyFingerprint, - ) - } - } - - override suspend fun deleteRepo(baseUrl: String) { - return handler.await { extension_reposQueries.delete(baseUrl) } - } - - private fun mapExtensionRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ): ExtensionRepo = ExtensionRepo( - baseUrl = baseUrl, - name = name, - shortName = shortName, - website = website, - signingKeyFingerprint = signingKeyFingerprint, - ) -} diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq b/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq deleted file mode 100644 index 6db69132a..000000000 --- a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq +++ /dev/null @@ -1,57 +0,0 @@ -CREATE TABLE extension_repos ( - base_url TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - short_name TEXT, - website TEXT NOT NULL, - signing_key_fingerprint TEXT UNIQUE NOT NULL -); - -findOne: -SELECT * -FROM extension_repos -WHERE base_url = :base_url; - -findOneBySigningKeyFingerprint: -SELECT * -FROM extension_repos -WHERE signing_key_fingerprint = :fingerprint; - -findAll: -SELECT * -FROM extension_repos; - -count: -SELECT COUNT(*) -FROM extension_repos; - -insert: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint); - -upsert: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint) -ON CONFLICT(base_url) -DO UPDATE -SET - name = :name, - short_name = :short_name, - website =: website, - signing_key_fingerprint = :fingerprint -WHERE base_url = base_url; - -replace: -INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) -VALUES (:base_url, :name, :short_name, :website, :fingerprint) -ON CONFLICT(signing_key_fingerprint) -DO UPDATE -SET - base_url = :base_url, - name = :name, - short_name = :short_name, - website =: website -WHERE signing_key_fingerprint = signing_key_fingerprint; - -delete: -DELETE FROM extension_repos -WHERE base_url = :base_url; diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_store.sq b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq new file mode 100644 index 000000000..e6cd2c471 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq @@ -0,0 +1,42 @@ +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +get: +SELECT * +FROM extension_store +WHERE index_url = :indexUrl; + +getAll: +SELECT * +FROM extension_store; + +getCount: +SELECT COUNT(*) +FROM extension_store; + +upsert: +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy) +ON CONFLICT(index_url) +DO UPDATE +SET + name = :name, + badge_label = :badgeLabel, + signing_key = :signingKey, + contact_website = :contactWebsite, + contact_discord = :contactDiscord, + is_legacy = :isLegacy +WHERE index_url = :indexUrl; + +delete: +DELETE FROM extension_store +WHERE index_url = :indexUrl; diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq index 44fa0570b..d5d9799dc 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq @@ -20,6 +20,9 @@ CREATE TABLE manga_sync( ON DELETE CASCADE ); +-- KMK: index for mangas.sq: getDuplicateLibraryManga's track_dupes +CREATE INDEX idx_manga_sync_sync_id_remote_id ON manga_sync(sync_id, remote_id, manga_id); + delete: DELETE FROM manga_sync WHERE manga_id = :mangaId AND sync_id = :syncId; diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index bf04d800e..d543b6690 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -119,12 +119,23 @@ AND source = :sourceId; getDuplicateLibraryManga: WITH +track_dupes AS ( + SELECT DISTINCT S2.manga_id + FROM manga_sync S1 + INNER JOIN manga_sync S2 + ON S1.sync_id = S2.sync_id + AND S1.remote_id = S2.remote_id + AND S1.manga_id != S2.manga_id + WHERE S1.manga_id = :id +), duplicates AS ( - SELECT * - FROM mangas + SELECT M.* + FROM mangas M + LEFT JOIN track_dupes D + ON D.manga_id = _id WHERE favorite = 1 AND _id != :id - AND lower(title) LIKE '%' || lower(:title) || '%' + AND (lower(title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL) ), chapter_counts AS ( SELECT diff --git a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm index 2614232b4..56b845447 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/45.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/45.sqm @@ -7,3 +7,5 @@ CREATE INDEX IF NOT EXISTS idx_mangas_categories_category_id ON mangas_categorie DROP INDEX IF EXISTS excluded_scanlators_manga_id_index; CREATE INDEX IF NOT EXISTS idx_excluded_scanlators_manga_id_scanlator ON excluded_scanlators(manga_id, scanlator); CREATE INDEX IF NOT EXISTS idx_history_last_read ON history(last_read) WHERE last_read > 0; + +CREATE INDEX IF NOT EXISTS idx_manga_sync_sync_id_remote_id ON manga_sync(sync_id, remote_id, manga_id); diff --git a/data/src/main/sqldelight/tachiyomi/migrations/46.sqm b/data/src/main/sqldelight/tachiyomi/migrations/46.sqm index e8c922056..abe80fe3e 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/46.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/46.sqm @@ -1 +1,16 @@ -ALTER TABLE history ADD COLUMN pages_read INTEGER NOT NULL DEFAULT 0; +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +SELECT base_url || '/repo.json', name, coalesce(short_name, name), signing_key_fingerprint, website, NULL, 1 FROM extension_repos; + +DROP TABLE extension_repos; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/47.sqm b/data/src/main/sqldelight/tachiyomi/migrations/47.sqm new file mode 100644 index 000000000..e8c922056 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/47.sqm @@ -0,0 +1 @@ +ALTER TABLE history ADD COLUMN pages_read INTEGER NOT NULL DEFAULT 0; diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt similarity index 86% rename from app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt rename to domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 3f63534ee..fbf3298d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model import android.graphics.drawable.Drawable import eu.kanade.tachiyomi.source.Source +import mihon.domain.extension.model.ExtensionStore import tachiyomi.domain.source.model.StubSource sealed class Extension { @@ -16,7 +17,7 @@ sealed class Extension { // KMK --> abstract val signatureHash: String - abstract val repoName: String? + abstract val storeName: String? // KMK <-- data class Installed( @@ -29,8 +30,8 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - /** Guessing repo name from built-in signatures preset */ - override val repoName: String? = null, + /** Guessing store name from built-in signatures preset */ + override val storeName: String? = null, // KMK <-- val pkgFactory: String?, val sources: List, @@ -38,7 +39,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isShared: Boolean, - val repoUrl: String? = null, + val store: ExtensionStore? = null, // SY --> val isRedundant: Boolean = false, // SY <-- @@ -54,12 +55,12 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - override val repoName: String, + override val storeName: String, // KMK <-- val sources: List, - val apkName: String, + val apkUrl: String, val iconUrl: String, - val repoUrl: String, + val store: ExtensionStore, ) : Extension() { data class Source( @@ -87,7 +88,7 @@ sealed class Extension { /* KMK --> */ override /* KMK <-- */ val signatureHash: String, // KMK --> - override val repoName: String? = null, + override val storeName: String? = null, // KMK <-- override val lang: String? = null, override val isNsfw: Boolean = false, diff --git a/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt new file mode 100644 index 000000000..7c10703e5 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class AddExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String): Result { + return repository.insert(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt new file mode 100644 index 000000000..2871e7f67 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt @@ -0,0 +1,9 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStoreCountAsFlow( + private val repository: ExtensionStoreRepository, +) { + operator fun invoke() = repository.getCountAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt new file mode 100644 index 000000000..e463af8d6 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt @@ -0,0 +1,13 @@ +package mihon.domain.extension.interactor + +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend fun get(): List = repository.getAll() + + fun subscribe(): Flow> = repository.getAllAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt new file mode 100644 index 000000000..42a8c07e2 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class RemoveExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String) { + repository.remove(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt new file mode 100644 index 000000000..7bbd2812e --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class UpdateExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke() { + repository.refreshAll() + } +} diff --git a/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt new file mode 100644 index 000000000..1ec6a37f0 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt @@ -0,0 +1,21 @@ +package mihon.domain.extension.model + +data class ExtensionStore( + val indexUrl: String, + val name: String, + val badgeLabel: String, + val signingKey: String, + val contact: Contact, + val isLegacy: Boolean, +) { + data class Contact( + val website: String, + val discord: String?, + ) +} + +const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" + +// cuong-tran's key +const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" +const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" diff --git a/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt new file mode 100644 index 000000000..69a59db06 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt @@ -0,0 +1,27 @@ +package mihon.domain.extension.repository + +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore + +interface ExtensionStoreRepository { + suspend fun insert(indexUrl: String): Result + + suspend fun insertFromPreference(indexUrl: String, name: String) + + suspend fun refreshAll() + + suspend fun fetchExtensions( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): List + + suspend fun getAll(): List + + fun getAllAsFlow(): Flow> + + fun getCountAsFlow(): Flow + + suspend fun remove(indexUrl: String) +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt deleted file mode 100644 index 4c6990be0..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt +++ /dev/null @@ -1,10 +0,0 @@ -package mihon.domain.extensionrepo.exception - -import java.io.IOException - -/** - * Exception to abstract over SQLiteException and SQLiteConstraintException for multiplatform. - * - * @param throwable the source throwable to include for tracing. - */ -class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt deleted file mode 100644 index befffb4dd..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt +++ /dev/null @@ -1,80 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import logcat.LogPriority -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import tachiyomi.core.common.util.system.logcat - -class CreateExtensionRepo( - private val repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() - - suspend fun await(indexUrl: String): Result { - val formattedIndexUrl = indexUrl.toHttpUrlOrNull() - ?.toString() - ?.takeIf { it.matches(repoRegex) } - ?: return Result.InvalidUrl - - val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json") - return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl - } - - private suspend fun insert(repo: ExtensionRepo): Result { - return try { - repository.insertRepo( - repo.baseUrl, - repo.name, - repo.shortName, - repo.website, - repo.signingKeyFingerprint, - ) - Result.Success - } catch (e: SaveExtensionRepoException) { - logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" } - return handleInsertionError(repo) - } - } - - /** - * Error Handler for insert when there are trying to create new repositories - * - * SaveExtensionRepoException doesn't provide constraint info in exceptions. - * First check if the conflict was on primary key. if so return RepoAlreadyExists - * Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint - * If neither are found, there was some other Error, and return Result.Error - * - * @param repo Extension Repo holder for passing to DB/Error Dialog - */ - private suspend fun handleInsertionError(repo: ExtensionRepo): Result { - val repoExists = repository.getRepo(repo.baseUrl) - if (repoExists != null) { - return Result.RepoAlreadyExists - } - val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(repo.signingKeyFingerprint) - if (matchingFingerprintRepo != null) { - return Result.DuplicateFingerprint(matchingFingerprintRepo, repo) - } - return Result.Error - } - - sealed interface Result { - data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result - data object InvalidUrl : Result - data object RepoAlreadyExists : Result - data object Success : Result - data object Error : Result - } - - companion object { - const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" - - // cuong-tran's key - const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" - const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt deleted file mode 100644 index 4b4b678e1..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class DeleteExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(baseUrl: String) { - repository.deleteRepo(baseUrl) - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt deleted file mode 100644 index 25b919607..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import kotlinx.coroutines.flow.Flow -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class GetExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - fun subscribeAll(): Flow> = repository.subscribeAll() - - suspend fun getAll(): List = repository.getAll() -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt deleted file mode 100644 index a7c4e7c6d..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class GetExtensionRepoCount( - private val repository: ExtensionRepoRepository, -) { - fun subscribe() = repository.getCount() -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt deleted file mode 100644 index 112ea701c..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class ReplaceExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(repo: ExtensionRepo) { - repository.replaceRepo(repo) - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt deleted file mode 100644 index a393e69d5..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt +++ /dev/null @@ -1,30 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository -import mihon.domain.extensionrepo.service.ExtensionRepoService - -class UpdateExtensionRepo( - private val repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - - suspend fun awaitAll() = coroutineScope { - repository.getAll() - .map { async { await(it) } } - .awaitAll() - } - - suspend fun await(repo: ExtensionRepo) { - val newRepo = service.fetchRepoDetails(repo.baseUrl) ?: return - if ( - repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") || - repo.signingKeyFingerprint == newRepo.signingKeyFingerprint - ) { - repository.upsertRepo(newRepo) - } - } -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt deleted file mode 100644 index ec9ccca87..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package mihon.domain.extensionrepo.model - -data class ExtensionRepo( - val baseUrl: String, - val name: String, - val shortName: String?, - val website: String, - val signingKeyFingerprint: String, -) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt deleted file mode 100644 index 47be56dcf..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt +++ /dev/null @@ -1,47 +0,0 @@ -package mihon.domain.extensionrepo.repository - -import kotlinx.coroutines.flow.Flow -import mihon.domain.extensionrepo.model.ExtensionRepo - -interface ExtensionRepoRepository { - - fun subscribeAll(): Flow> - - suspend fun getAll(): List - - suspend fun getRepo(baseUrl: String): ExtensionRepo? - - suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? - - fun getCount(): Flow - - suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo(repo: ExtensionRepo) { - upsertRepo( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) - } - - suspend fun replaceRepo(newRepo: ExtensionRepo) - - suspend fun deleteRepo(baseUrl: String) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt deleted file mode 100644 index 6a0a492de..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt +++ /dev/null @@ -1,27 +0,0 @@ -package mihon.domain.extensionrepo.service - -import kotlinx.serialization.Serializable -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -data class ExtensionRepoMetaDto( - val meta: ExtensionRepoDto, -) - -@Serializable -data class ExtensionRepoDto( - val name: String, - val shortName: String?, - val website: String, - val signingKeyFingerprint: String, -) - -fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo { - return ExtensionRepo( - baseUrl = baseUrl, - name = meta.name, - shortName = meta.shortName, - website = meta.website, - signingKeyFingerprint = meta.signingKeyFingerprint, - ) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt deleted file mode 100644 index 2d86b89d1..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt +++ /dev/null @@ -1,36 +0,0 @@ -package mihon.domain.extensionrepo.service - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.parseAs -import kotlinx.serialization.json.Json -import logcat.LogPriority -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat - -class ExtensionRepoService( - networkHelper: NetworkHelper, - private val json: Json, -) { - val client = networkHelper.client - - suspend fun fetchRepoDetails( - repo: String, - ): ExtensionRepo? { - return withIOContext { - try { - with(json) { - client.newCall(GET("$repo/repo.json")) - .awaitSuccess() - .parseAs() - .toExtensionRepo(baseUrl = repo) - } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to fetch repo details" } - null - } - } - } -} diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt new file mode 100644 index 000000000..a27b3d13f --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetBookmarkedChaptersByMangaId.kt @@ -0,0 +1,33 @@ +package tachiyomi.domain.chapter.interactor + +import exh.source.MERGED_SOURCE_ID +import logcat.LogPriority +import tachiyomi.core.common.util.system.logcat +import tachiyomi.domain.chapter.model.Chapter +import tachiyomi.domain.chapter.repository.ChapterRepository +import tachiyomi.domain.manga.interactor.GetManga + +class GetBookmarkedChaptersByMangaId( + private val chapterRepository: ChapterRepository, + // SY --> + private val getManga: GetManga, + private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId, + // SY <-- +) { + + suspend fun await(mangaId: Long): List { + return try { + // SY --> + val manga = getManga.await(mangaId) ?: return emptyList() + if (manga.source == MERGED_SOURCE_ID) { + return getMergedChaptersByMangaId.await(mangaId, applyFilter = /* KMK --> */false /* KMK <-- */) + .filter { it.bookmark } + } + // SY <-- + chapterRepository.getBookmarkedChaptersByMangaId(mangaId) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } +} diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt index 39a283304..d6658141f 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetMergedChaptersByMangaId.kt @@ -19,6 +19,7 @@ class GetMergedChaptersByMangaId( suspend fun await( mangaId: Long, dedupe: Boolean = true, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): List { return transformMergedChapters( @@ -31,6 +32,7 @@ class GetMergedChaptersByMangaId( suspend fun subscribe( mangaId: Long, dedupe: Boolean = true, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): Flow> { return try { @@ -46,6 +48,7 @@ class GetMergedChaptersByMangaId( private suspend fun getFromDatabase( mangaId: Long, + /** Filter excluded scanlators & bookmarked/unbookmarked filter */ applyFilter: Boolean = false, ): List { return try { diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index 6055965ec..e95d26a7d 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -42,7 +42,7 @@ class DownloadPreferences( fun parallelPageLimit() = preferenceStore.getInt("download_parallel_page_limit", 5) // SY --> - fun includeChapterUrlHash() = preferenceStore.getBoolean("download_include_chapter_url_hash", true) + fun includeChapterUrlHash() = preferenceStore.getBoolean("download_include_chapter_url_hash", false) // SY <-- // KMK --> diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 95f690865..621d2ac30 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -7,7 +7,7 @@ interpolator_version = "1.0.0" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } -annotation = "androidx.annotation:annotation:1.9.1" +annotation = "androidx.annotation:annotation:1.10.0" appcompat = "androidx.appcompat:appcompat:1.7.1" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1" @@ -21,7 +21,7 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" } lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" } -workmanager = "androidx.work:work-runtime:2.11.1" +workmanager = "androidx.work:work-runtime:2.11.2" paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } @@ -31,7 +31,7 @@ interpolator = { group = "androidx.interpolator", name = "interpolator", version benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.1" test-ext = "androidx.test.ext:junit-ktx:1.3.0" test-espresso-core = "androidx.test.espresso:espresso-core:3.7.0" -test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0" +test-uiautomator = "androidx.test.uiautomator:uiautomator:2.4.0" [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 5fb461b74..fa180981a 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,8 +1,8 @@ [versions] -compose-bom = "2026.02.00" +compose-bom = "2026.06.01" [libraries] -activity = "androidx.activity:activity-compose:1.12.4" +activity = "androidx.activity:activity-compose:1.13.0" bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } @@ -17,4 +17,4 @@ material3-core = { module = "androidx.compose.material3:material3" } material-icons = { module = "androidx.compose.material:material-icons-extended" } glance = "androidx.glance:glance-appwidget:1.1.1" -colorpicker = {module = "com.github.skydoves:colorpicker-compose", version = "1.1.3"} +colorpicker = {module = "com.github.skydoves:colorpicker-compose", version = "1.2.0"} diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 6e772ba01..f32ab7994 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,7 +1,8 @@ [versions] -kotlin_version = "2.3.10" +kotlin_version = "2.4.0" serialization_version = "1.10.0" xml_serialization_version = "0.91.3" +coroutines_version = "1.11.0" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } @@ -10,11 +11,11 @@ compose-compiler-gradle = { module = "org.jetbrains.kotlin:compose-compiler-grad immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" } -coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.10.2" } -coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } -coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } -coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" } -coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "coroutines_version" } +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" } +coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" } +coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "coroutines_version" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines_version" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" } serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization_version" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dfd1f8a65..c6c0d28ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] aboutlib_version = "13.2.1" leakcanary = "2.14" -moko = "0.26.0" +moko = "0.26.4" okhttp_version = "5.3.2" shizuku_version = "13.1.5" sqldelight = "2.2.1" sqlite = "2.6.2" voyager = "1.1.0-beta03" -spotless = "8.2.1" +spotless = "8.8.0" ktlint-core = "1.8.0" -firebase-bom = "34.9.0" +firebase-bom = "34.15.0" markdown = "0.39.2" junit = "6.0.3" -materialKolor = "5.0.0-alpha06" +materialKolor = "5.0.0-alpha07" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.1.5" @@ -30,7 +30,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3" quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" } -jsoup = "org.jsoup:jsoup:1.22.1" +jsoup = "org.jsoup:jsoup:1.22.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.komikku-app:UniFile:084a54140a" @@ -44,7 +44,7 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1" injekt = "com.github.mihonapp:injekt:91edab2317" -coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.3.0" } +coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.5.0" } coil-core = { module = "io.coil-kt.coil3:coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif" } coil-compose = { module = "io.coil-kt.coil3:coil-compose" } @@ -55,7 +55,7 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" -richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc13" +richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc14" material = "com.google.android.material:material:1.13.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" @@ -90,12 +90,12 @@ leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", versi sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" } -sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version = "2.2.1" } +sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version = "2.3.2" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } -kotest-assertions = "io.kotest:kotest-assertions-core:6.1.3" -mockk = "io.mockk:mockk:1.14.9" +kotest-assertions = "io.kotest:kotest-assertions-core:6.2.1" +mockk = "io.mockk:mockk:1.14.11" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } @@ -113,11 +113,11 @@ stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } [plugins] -google-services = { id = "com.google.gms.google-services", version = "4.4.4" } +google-services = { id = "com.google.gms.google-services", version = "4.5.0" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlib_version" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.7" } [bundles] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] diff --git a/gradle/sy.versions.toml b/gradle/sy.versions.toml index ab714cbcd..515fed135 100644 --- a/gradle/sy.versions.toml +++ b/gradle/sy.versions.toml @@ -6,12 +6,12 @@ xlog = "com.elvishew:xlog:1.11.1" ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.3.12" -gradleversionsx = "com.github.ben-manes:gradle-versions-plugin:0.53.0" +gradleversionsx = "com.github.ben-manes:gradle-versions-plugin:0.54.0" sqlcipher = "net.zetetic:sqlcipher-android:4.13.0" exifinterface = "androidx.exifinterface:exifinterface:1.4.2" -google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20251210-2.0.0" +google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev20260624-2.0.0" zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0" diff --git a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml index df7a724f5..6667dd4c8 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ar/strings.xml @@ -121,7 +121,7 @@ نسخ الصفحة المدمجة إلى الحافظة عدم التحميل التلقائي للتحديث إزالة العناصر الأصلية من المكتبة؟ - الدعم: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + الدعم: MangaDex, E-H, ExH, nH, 8Muses فتح مجلد تحديث مانجا غير مكتملة @@ -162,4 +162,41 @@ البيانات التي تم تنزيلها الفصول والسجل من قاعدة البيانات تثبيت يدوي + خدمة ديسكورد هذه تعمل من خلال تسجيل الرموز. استخدمها على مسؤوليتك الخاصة. يتم تخزين الرمز المميز الخاص بك محليا ولا يقوم %s بمشاركته في أي مكان آخر + وجود ديسكورد الغني + إظهار عنوان الحلقة التي تشاهدها + حسابات ديسكورد + الحساب النشط + التمرير عبر + تم تجاوزها عند تمكين وضع التخفي + اتصالات + 🌙 الخمول + إظهار التقدم + إظهار الوقت + لم يتم العثور على حسابات + فشل في تسجيل الدخول + الخدمات الخاصة + ديسكورد، أكثر من ذلك.. + ديسكورد + تخصيص ديسكورد + رسالة مخصصة + رسالة مخصصة سيتم عرضها على ديسكورد + إظهار الأزرار + عرض الأزرار لتنزيل التطبيق أو الانضمام إلى خادم ديسكورد + يعرض زر لتنزيل التطبيق + إظهار زر ديسكورد + تمكين وجود ديسكورد الغني + حالة ديسكورد + ⛔ الرجاء عدم الإزعاج + 🟢 متصل + وضع التخفي RPC + المدخلات في الفئات المدرجة لن يتم عرضها في ديسكورد + تصفح + المراسلة مع + إظهار زر التحميل + يعرض زرًا للانضمام إلى خادم ديسكورد + وضع التخفي ديسكورد + باستخدام: + إدعمني + %1$s %2$s diff --git a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml index c428c24a1..143ee9d61 100644 --- a/i18n-kmk/src/commonMain/moko-resources/as/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/as/strings.xml @@ -136,5 +136,78 @@ বৰ্তমান: %1$s আপডেট কৰিবলগীয়া মোক স্পনছৰ কৰক - সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses + ডিস্ক’ৰ্ড ৰিচ প্ৰেজেন্স + সক্ৰিয় একাউণ্ট + সংযোগ + ডিস্ক’ৰ্ড, আৰু আহিব... + 🌙 নিষ্ক্ৰিয় + 🟢 অনলাইন + ডিস্ক’ৰ্ড একাউণ্ট + ডিস্ক’ৰ্ড + এই ডিস্ক\'ৰ্ড সেৱা ট\'কেন লগিঙৰ জৰিয়তে কাম কৰে। নিজৰ ঝুঁকিত ব্যৱহাৰ কৰক। আপোনাৰ ট\'কেন স্থানীয়ভাৱে সাঁচৱা হয় আৰু %s এ ইয়াক অন্য ক\'ত শ্বেয়াৰ নকৰে + ইনকগনিটো মোড সক্ৰিয় হ’লে ওভাৰৰাইড কৰা হয় + ব্ৰাউজ কৰি আছে + মাজেৰে স্ক্ৰ’ল কৰি আছে + ডিস্ক’ৰ্ড কাষ্টমাইজেশ্যন + কাষ্টম বাৰ্তা + বিশেষ সেৱা + অন্তৰ্ভুক্ত শ্ৰেণীৰ এণ্ট্ৰি ডিস্ক’ৰ্ডত দেখুৱা নহ’ব + কোনো একাউণ্ট পোৱা নগ’ল + লগইন বিফল + ডিস্ক’ৰ্ডত দেখুৱা হ’বলগীয়া কাষ্টম বাৰ্তা + প্ৰগ্ৰেছ দেখুৱাওক + সময় দেখুৱাওক + বাটন দেখুৱাওক + এপ ডাউনলোড বা ডিস্ক’ৰ্ড চাৰ্ভাৰত যোগদানৰ বাটন দেখুৱাওক + ডিস্ক’ৰ্ড বাটন দেখুৱাওক + আৰ পি চি ইনকগনিটো + ডাউনলোড বাটন দেখুৱাওক + এপ ডাউনলোডৰ বাবে এটা বাটন দেখুৱায় + ডিস্ক’ৰ্ড ইনকগনিটো মোড + খেলা-ধূলা কৰি আছে + ডিস্ক’ৰ্ড চাৰ্ভাৰত যোগদানৰ বাবে এটা বাটন দেখুৱায় + আপুনি পঢ়ি থকা অধ্যায়ৰ শিৰোনাম দেখুৱাওক + ডিস্ক’ৰ্ড ৰিচ প্ৰেজেন্স সক্ৰিয় কৰক + ডিস্ক’ৰ্ডৰ স্থিতি + ⛔ বিৰক্ত নকৰিব + ইউজাৰনেম + পাছৱাৰ্ড + %1$s %2$s + অসম্পূৰ্ণ মাংগা + অসম্পূৰ্ণ অধ্যায় + পুথিভঁৰালৰ বাহিৰৰ এণ্ট্ৰি + পিঞ্চ টু জুম + জুম ইন নিষ্ক্ষিয় কৰক + বহল ছবিৰ জুম মোড + ডাবল জুম + বহল স্ক্ৰীনত স্মাৰ্ট স্কেল + দীঘল ষ্ট্ৰিপত ফাঁকসহ স্মাৰ্ট স্কেল প্ৰয়োগ + ১৬:৯ + ২০:৯ + ৪:৩ + ৩:২ + সন্ধান/ফিল্টাৰৰ সময়ত খালী শ্ৰেণী দেখুৱাওক + পুথিভঁৰালত যোগ কৰোঁতে মেটাডেটা আনক + পুথিভঁৰালত নতুন মাংগা যোগ কৰোঁতে স্বয়ংক্ৰিয়ভাৱে মেটাডেটা আনে + পুথিভঁৰালত যোগ কৰোঁতে অধ্যায় আনক + পুথিভঁৰালত নতুন মাংগা যোগ কৰোঁতে স্বয়ংক্ৰিয়ভাৱে অধ্যায়ৰ তালিকা আনে + ৱেবডেভি + ৱেবডেভি চাৰ্ভাৰ ইউআৰএল + ৱেবডেভি চাৰ্ভাৰৰ ঠিকনা দিয়ক + আপোনাৰ ৱেবডেভি একাউণ্টৰ ইউজাৰনেম দিয়ক + আপোনাৰ ৱেবডেভি একাউণ্টৰ পাছৱাৰ্ড দিয়ক + ৱেবডেভি ডেটা ফ\'ল্ডাৰ + এপ্লিকেশ্যনে এই ফ\'ল্ডাৰত তথ্য সাঁচিব + স্মাৰ্ট সন্ধান ব্যৱহাৰ কৰক + সক্ৰিয় কৰিলে স্বয়ংক্ৰিয়ভাৱে সৰ্বোত্তম ফলাফল বাছি লয়। অন্যথা, মেনুৱেল বাছনিৰ বাবে সকলো ফলাফল দেখুৱায়। + স্বয়ংক্ৰিয় পুথিভঁৰাল আপডেট নিৰ্ধাৰণ বিফল: %s + স্বয়ংক্ৰিয় বেকআপ নিৰ্ধাৰণ বিফল: %s + ডিস্ক\'ৰ্ড আৰ পি চি কনফিগাৰ কৰিবলৈ খোলক + অধ্যায়ৰ শিৰোনাম দেখুৱাওক + ব্যৱহাৰ কৰি আছে + %s-ত পঢ়ক + কমিককু - এটা মাংগা ৰিডাৰ এপ + বৰ্তমান পঢ়ি থকা অধ্যায় দেখুৱাওক + কাৰ্যকলাপৰ সময় দেখুৱাওক diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index afc666e9d..c254abf06 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -198,7 +198,7 @@ Failed to schedule automatic backup: %s To be updated Sponsor Me - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Special Services Connections diff --git a/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml b/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml index 55344e519..4e1c4a4c2 100644 --- a/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/bn/strings.xml @@ -1,3 +1,50 @@ - \ No newline at end of file + ১২ ঘন্টা + ১ দিন + খোল + সংযোগ + %1$s %2$s + আপডেট সম্পন্ন হয়েছে + ডিসকর্ড + সূর্যাস্ত + মনোক্রোম + নিজ হাতে নিজের চেষ্টায় ইন্সটল + নিরপেক্ষ + প্রাণবন্ত + উপরে যান + লুকান + ফোল্ডার খুলুন + অভিব্যক্তিপূর্ণ + রংধনু + কাস্টম থিম + রাস্পবেরি + আপডেট ইনস্টল করা যায়নি + পরামর্শ + ডাউনলোড বোতাম দেখও + ওয়েব-ড্যাভ + নাম + পাসওয়ার্ড + রিলিজ পাতা + উপরে  স্ক্রল করুন + বিঘ্নমুক্ত পরিবেশ + নিষ্ক্রিয় + সক্রিয় + দেখছি + ডিসকর্ড রিচ প্রেজেন্স + টোনাল স্পট + ফলের সালাদ + বিশ্বস্ততা + বিষয়বস্তু + ম্যানুয়াল + ১ ঘন্টা + FAQ এবং নির্দেশনা + নীচে যান + আপডেট + ৩ঃ২ + আপডেট করা + ৬ ঘন্টা + স্বয়ংক্রিয় আপডেট অ্যাপ + সব এড়িয়ে যান + ২ ঘণ্টা + diff --git a/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml index 3d5e2dc09..4cdade365 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ca/strings.xml @@ -35,4 +35,126 @@ Pessiga per ampliar Desactivar el zoom Doble zoom + Manual + Auto-update app + Permission granted. Try action again. + If set to \"manual\", either refresh each manga or using the \"Invalidate downloads index\" action in \"Advanced\" + Allow using custom covers in entry\'s Edit Info + More like these... + Allow all + Manga Info + Monochrome + Scroll to next + Skip it + Tonal Spot + Home from Suggestions + Suggestions + Panorama Cover + LCD + Library search + Toggle NSFW only + Custom theme style + Theme based on cover + Cover based theme style + Neutral + Vibrant + Show restoring progress banner + Show syncing progress banner + Clean invalid downloads + Invalid downloads cleaned + Avoid using Private Installer because it\'s not stable yet, will lead to really slow start/resume App. + Allow it + Skip all + Update completed + Release page + Could not install update + Coming (soon) updates! + Manual download + Show source-website\'s Suggestions while viewing entry + Scroll to top + Scroll to bottom + Source icon + Find and remove all downloads, files, folders which are not saved in your library + Preload library\'s cover color + Don\'t auto download update + Access media files permission + Website Suggestions + Scroll to previous + To be updated + Expressive + Rainbow + Fidelity + Show cover in landscape mode if it\'s a wide image + Panorama Cover Flow + Show panorama cover in Global Search & Suggestions + Panorama comfortable grid + Expand Suggestions + If turned off, Suggestions won\'t automatically load & show expanded + Suggestions in overflow + Put the Suggestions button in the overflow menu instead of on the entry page + Show an icon while browsing Suggestions to navigate directly back to Home + Cloudflare + Doom + Palette Personalizer + Pick a seed color for the custom Theme + Custom Theme + Casual + Outrun + Raspberry + Cathode + Bubblegum + Night at the beach + Springfield + PICO-8 + Show updating progress banner + Sync progress from Trackers up to chapter %d + Saved Searches & Feeds + Allow saving covers & using custom covers in entry\'s Edit Info + What\'s coming (soon) + Library update errors + You have no library update errors. + Too many sources in your feed, cannot add more than 20 + Sort feeds + Would you like to sort the feeds alphabetically? + More extensions... + Download cache renew interval + Change Logs + Saved Searches (Hold to add to Feed) + Saved Searches (Hold to delete) + Library update errors (%d) + Current: %1$s + Try now + Auto sync progress from Trackers + Mocha + Spectral + Patagonia + Fruit Salad + Content + Pop + Use language icon + Refresh extension page to update list. + Inactiu + Cap compte trobat + Navegació + Instal·lació manual + Actualització + No molesteu + En línia + 3:2 + WebDAV + Nom d\'usuari + Contrasenya + Discord + Estat d\'activitat de Discord + Fallida l\'accedir + Mitjançant: + Mostra l\'hora + Mostra els botons + 16:9 + Compte actiu + %1$s %2$s + Mostra el botó de descàrrega + Obrir el menú de rastreig al afegir a la biblioteca + Activa la Discord Rich Presence + Connexions diff --git a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml index 0b020bcd7..5025a613f 100644 --- a/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/cs/strings.xml @@ -115,5 +115,62 @@ Kopírovat kombinovanou stránku do schránky Automaticky nestahovat aktualizaci Odstranit původní položky z knihovny? - Podpora: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Podpora: MangaDex, E-H, ExH, nH, 8Muses + Bubblegum + Discord účty + Vlastní zpráva, která bude zobrazena na Discordu + Zobrazit Discord tlačítko + Zobrazí tlačítko pro připojení k Discord serveru + Outrun + Pop + Discord, více k nám. + Discord + Tato Discord služba funguje pomocí logování tokenů. Používejte na vlastní riziko. Váš token je uložen lokálně a Anikku jej nesdílejí nikde jinde + Discord Rich Presence + ⛔ Režim Nerušit + 🌙 Nečinný + Zobrazit tlačítko Stahování + Zobrazí tlačítko ke stažení aplikace + Speciální služby + Kontakty + Zobrazit název epizody, kterou sledujete + Povolit Discord Rich Presence + Stav Discordu + RPC Incognito + Discord anonymní režim + Přepsáno při povoleném anonymním režimu + Aktivní účet + Nebyly nalezeny žádné účty + Přihlášení se nezdařilo + Procházení + Posunutí přes + Vlastní zpráva + Zobrazit čas + Zobrazit tlačítka ke stažení aplikace nebo se připojit k Discord serveru + Zobrazit obal zarovnaný nahoře vedle informací anime + Doom + Springfield + Poslání s + Zobrazit průběh + Mocha + 🟢 Online + Položky v zahrnutých kategoriích nebudou zobrazeny v Discordu + Přizpůsobení Discordu + Sapphire + Zobrazit tlačítka + Uživatelské jméno + Heslo + Ruční instalace + Podpoř mě + 16:9 + Otevřít nabídku stopování při přidávání do knihovny + Otevřít složku + Aktualizace + 3:2 + WebDAV + %1$s %2$s + Používání + Číst na %s + Aktualizace + Nelze otevřít složku. diff --git a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml index 0df4bf402..f3faca7f9 100644 --- a/i18n-kmk/src/commonMain/moko-resources/da/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/da/strings.xml @@ -115,5 +115,58 @@ Kopier kombineret side til udklipsholder Download ikke automatisk opdatering Fjern originale indgange fra biblioteket? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses + Sapphire + Særlige Tjenester + Indgange i inkluderede kategorier vil ikke blive vist i Discord + Rul gennem + Mocha + Springfield + Discord Rig Tilstedeværelse + Forbindelser + Discord, mere at komme. + Denne Discord Service virker gennem token logning. Bruges på egen risiko. Dit token gemmes lokalt, og Anikku deler det ikke andre steder + Aktiver Discord Rig Tilstedeværelse + 🟢 Online + Viser en knap til download af appen + Discord Status + ⛔ Forstyr Ikke + 🌙 Tomgang + Tilsidesæt når Inkognitotilstand er aktiveret + Ingen konti fundet + Gennemser + Beskeder rundt med + Brugerdefineret Besked + Brugerdefineret besked, der vil blive vist på Discord + Vis Tid + Vis Knapper + Vis knapper til at downloade appen, eller deltag i Discord serveren + Vis Download-Knap + Viser en knap til at deltage i Discord serveren + Tonal Spot + Vis coveret justeret til toppen sammen med anime info + Outrun + Pop + Bubblegum + Kunne ikke logge ind + Discord Konti + Vis titlen på den episode du ser + Aktiv Konto + Discord Tilpasning + Vis Discord Knap + RPC Incognito + Discord Inkognito tilstand + Vis Fremskridt + Discord + WebDAV + Brugernavn + Adgangskode + Manuel installation + %1$s %2$s + Bruger: + Opdaterer + Åbn mappe + Opdater + 16:9 + 3: 2 diff --git a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml index 50e0141fb..5941c794d 100644 --- a/i18n-kmk/src/commonMain/moko-resources/de/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/de/strings.xml @@ -122,7 +122,7 @@ Kombinierte Seite in Zwischenablage kopieren Update nicht automatisch herunterladen Originale Einträge aus der Bibliothek entfernen? - Unterstützung: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Unterstützung: MangaDex, E-H, ExH, nH, 8Muses Panorama-Cover Flow Untergang Paletten-Personalisierer @@ -212,4 +212,7 @@ Max. Seiten pro Verlauf-Stichprobe Verlaufseinträge mit mehr gelesenen Seiten werden für Seitenraten-Schätzungen übersprungen. %d Seiten + WebDAV + Benutzername + Passwort diff --git a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml index 306c70fa9..e87e186c2 100644 --- a/i18n-kmk/src/commonMain/moko-resources/el/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/el/strings.xml @@ -116,5 +116,57 @@ Αντιγραφή συνδυασμένης σελίδας στο πρόχειρο Να μην γίνεται αυτόματη λήψη ενημέρωσης Αφαιρέστε τις αρχικές καταχωρήσεις από τη βιβλιοθήκη? - Υποστήριξη: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Υποστήριξη: MangaDex, E-H, ExH, nH, 8Muses + Sapphire + Ειδικές Υπηρεσίες + Discord + Discord Πλούσια Παρουσία + 🟢 Online + Δεν βρέθηκαν λογαριασμοί + Outrun + Κύλιση μέσα + Εμφάνιση Κουμπιών + Προσαρμοσμένο Μήνυμα + Εμφάνιση Προόδου + Εμφάνιση κουμπιών για λήψη της εφαρμογής ή σύνδεση με τον διακομιστή Discord + Εμφανίζει ένα κουμπί για να συνδεθεί με τον διακομιστή του Discord + Discord, περισσότερα για να έρθει.. + Αυτή η υπηρεσία του Discord λειτουργεί μέσω καταγραφής συμβόλων. Χρησιμοποιήστε το με δική σας ευθύνη. Το διακριτικό αποθηκεύεται τοπικά και το Anikku δεν το μοιράζεται πουθενά αλλού + Εμφάνιση του τίτλου του επεισοδίου που παρακολουθείτε + Ενεργοποίηση Discord Rich Presence + Κατάσταση Discord + ⛔ Μην Ενοχλείτε + 🌙 Αδρανής + RPC Incognito + Ανώνυμη περιήγηση σε Discord + Αντικαταστάθηκε όταν είναι ενεργοποιημένη η λειτουργία ανώνυμης περιήγησης + Οι καταχωρήσεις στις κατηγορίες που περιλαμβάνονται δεν θα εμφανίζονται στο Discord + Λογαριασμοί Discord + Ενεργός Λογαριασμός + Ανταλλαγή μηνυμάτων με + Προσαρμογή Discord + Προσαρμοσμένο μήνυμα που θα εμφανίζεται στο Discord + Εμφάνιση Κουμπιού Λήψης + Εμφανίζει ένα κουμπί για λήψη της εφαρμογής + Tonal Spot + Show panorama cover in Global Search & Suggestions + Springfield + Εμφάνιση του καλύμματος που είναι ευθυγραμμισμένο στην κορυφή παράλληλα με τις πληροφορίες anime + Cathode + Εμφάνιση Ώρας + Συνδέσεις + Εμφάνιση Κουμπιού Discord + Αποτυχία σύνδεσης + Περιήγηση + Όνομα χρήστη + Χειροκίνητη εγκατάσταση + Ενημέρωση + 16:9 + WebDAV + Κωδικός + %1$s %2$s + 3:2 + Γίνεται ενημέρωση + Άνοιγμα φακέλου + Χρησιμοποιεί: diff --git a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml index 267ee0a81..ba2e37e2a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/es/strings.xml @@ -25,7 +25,7 @@ ¡Próximas actualizaciones (pronto)! Registro de cambios Más así... - Demasiadas fuentes en tu feed, no se pueden agregar más de 20 + Demasiadas fuentes en novedades, no se pueden poner más de 20 Actual: %1$s Tema según la portada Spot Tonal @@ -116,7 +116,7 @@ Copiar página combinada al portapapeles No descargar actualización automática ¿Eliminar entradas originales de la biblioteca? - Soporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Soporte: MangaDex, E-H, ExH, nH, 8Muses Abrir carpeta Actualizar Fallo al abrir la carpeta @@ -135,4 +135,51 @@ Abrir menú de seguimiento al añadir a la biblioteca Datos descargados Error al iniciar sesión + Nombre de usuario + Contraseña + Instalación manual + WebDAV + Patrociname + Sigue leyendo en %s + Utilización + %1$s %2$s + Reemplazado cuando el modo Incognito está activado + 🌙 Inactivo + 🟢 Online + Mensaje personalizado + RPC Incognito + Pop + Outrun + Discord, más por venir.. + Este Servicio de Discord funciona a través del registro de token. Úsalo bajo tu propio riesgo. Tu token se almacena localmente y Anikku no lo comparte en ningún otro lugar + Episodios e historial de la base de datos + Conexiones + Discord + Presencia de Discord Rich + Habilitar la presencia de Discord Rich + ⛔ No molestar + Modo Incognito de Discord + Las entradas en las categorías incluidas no se mostrarán en Discord + Desplazándose a través + Personalización de Discord + Mensaje personalizado que se mostrará en Discord + Mostrar progreso + Mostrar hora + Patagonia + Springfield + Muestra el título del episodio que estás viendo + Estado de Discord + Servicios Especiales + Navegando + Cuenta activa + No hay cuentas encontradas + Muestra un botón para unirse al servidor de Discord + Doom + Cuentas de Discord + Mensajes alrededor con + Mostrar botones + Mostrar botones para descargar la aplicación o unirse al servidor de Discord + Mostrar botón de descarga + Muestra un botón para descargar la aplicación + Mostrar botón de Discord diff --git a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml index a57321d05..96fc35ae0 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fi/strings.xml @@ -114,5 +114,59 @@ Kopioi yhdistetty sivu leikepöydälle Älä lataa automaattisesti päivitystä Poistetaanko alkuperäiset merkinnät kirjastosta? - Tuki: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Tuki: MangaDex, E-H, ExH, nH, 8Muses + Discord Rich Presence + Kirjautuminen epäonnistui + Sapphire + Outrun + Bubblegum + Patagonia + Ohita kun Incognito-tila on käytössä + Tilejä ei löytynyt + Mukautettu viesti, joka näytetään Discordissa + Näytä Edistyminen + Näytä painikkeet, joilla voit ladata sovelluksen tai liittyä Discord-palvelimeen + Discord + Tämä Discord-palvelu toimii tunnuksen lokilla. Käytä omalla vastuullasi. Tunnuksesi tallennetaan paikallisesti ja %s ei jaa sitä mistään muualta. + Discordin Tila + ⛔ Älä Häiritse + Discord Incognito -tila + Discordin Mukauttaminen + Mukautettu Viesti + Näytä Latauspainike + Näyttää painikkeen, jolla sovellus ladataan + Näyttää painikkeen, jolla voit liittyä Discord-palvelimeen + Näytä kansi, joka on linjattu yläreunaan anime info + Spectral + Yhteydet + Discord, lisää tullaan.. + Ota Discordin Rich Presence Käyttöön + 🌙 Käyttämätön + 🟢 Online + RPC Incognito + Sisällytettyjä kategorioita ei näytetä Discordissa + Vieritä läpi + Näytä Aika + Näytä Discord-painike + Mocha + Springfield + Discordin Tilit + Aktiivinen Tili + Selataan + Näytä sen jakson otsikko, jonka katselet + Doom + Erityiset Palvelut + Messing noin kanssa + Näytä Painikkeet + Päivitä + WebDAV + Käyttäjätunnus + Salasana + Manuaalinen asennus + Käyttäen: + 16:9 + 3:2 + Päivitetään + %1$s %2$s + Avaa kansio diff --git a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml index 35461dd78..476858f3a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fil/strings.xml @@ -97,7 +97,7 @@ Kopyahin ang pinagsamang pahina sa clipboard Huwag awtomatikong i-download ang update Tanggalin ang orihinal na mga tala galing sa aklatan? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Fruit Salad Makulay Masining @@ -182,4 +182,26 @@ Awtomatikong kunin at i-update ang metadata ng manga kapag idinagdag mo ito sa iyong aklatan Kunin ang mga kabanata kapag idinadagdag ang manga sa aklatan Awtomatikong i-scan ang source para sa mga kabanata kapag idinagdag ang manga sa iyong aklatan + Monochrome + Fidelity + Panorama Cover + Panorama Cover Flow + Panorama comfortable grid + Doom + Mocha + Outrun + Raspberry + Pop + Cathode + Bubblegum + Spectral + Patagonia + Springfield + Discord + %1$s %2$s + Username (pangalan) + Password + Gamitin ang matalinong paghahanap + Kapag napagana, awtomatikong pipiliin ang pinaka-akmang resulta. Kung hindi, ipakikita ang lahat ng resulta para manual na makapili. + Folder ng WebDAV data diff --git a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml index b7baa9ace..105a2bb82 100644 --- a/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/fr/strings.xml @@ -114,7 +114,7 @@ Copier la page combinée dans le presse-papiers Ne pas télécharger automatiquement la mise à jour Supprimer les entrées originales de la bibliothèque ? - Compatible : MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Compatible : MangaDex, E-H, ExH, nH, 8Muses Tonal Monochrome Doom @@ -192,4 +192,7 @@ Afficher un bouton pour télécharger l\'application Afficher le bouton Discord Afficher un bouton pour rejoindre le serveur Discord + WebDAV + Nom d\'utilisateur + Mot de passe diff --git a/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml index a6b3daec9..77b71f88b 100644 --- a/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/hi/strings.xml @@ -1,2 +1,60 @@ - \ No newline at end of file + + 12 घंटे + हस्तचालित + 1 घंटा + सभी छोड़ें + अयांत्रिक संस्थापन + सुझाव + ऊपर स्क्रॉल करें + नीचे तक जाएं + ब्राउज़िंग + प्रगति दिखाएं + मेघधनुष + एक रंग का + निष्क्रिय + ऊपर ले जाएँ + चमकीला + कयामत + सामान्य प्रश्नोत्तर और मार्गदर्शन + तटस्थ + अद्यतन पूर्ण हुआ + फ़ोल्डर खोलें + अद्यतन कर रहे हैं + बटन दिखाएं + Show download button + 3:2 + उपयोगकर्ता नाम + पासवर्ड + वर्तमान: %1$s + कनेक्शन + डिस्कॉर्ड + डिस्कॉर्ड रिच प्रेजेंस + %1$s %2$s + उपयोग में: + माँगा जानकारी + टोनल स्पॉट + अभिव्यंजक + फल सलाद + फिडेलिटी + सामग्री + आव्यूह + नीलम + कस्टम थीम + सूर्यास्त + रसभरी + १ दिन + डिस्टर्ब न करें + ऑनलाइन + 2 घंटे + 6 घंटे + मैनुअल डाउनलोड + अपडेट इंस्टॉल नहीं किया जा सका + समय दिखाएं + खोलें + रिलीज़ पेज + नीचे ले जाएँ + छुपाएं + सभी की अनुमति दें + अद्यतन + diff --git a/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml index 0a030ddb5..4b654cfc1 100644 --- a/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/hr/strings.xml @@ -2,4 +2,66 @@ Cloudflare Previše izvora u tvom feedu, ne možeš dodati više od 20 + Ručno preuzimanje + Još više proširenja + 1 sat + 2 sata + 12 sati + 1 dan + WebDAV + Preskoči sve + Stranica izdanja + Nije moguće instalirati ažuriranje + 16:9 + Jednobojno + Online + Neutralni + ČPP i vodiči + Pomakni gore + Sakrij + Dopusti sve + Otvori mapu + Nadopunjivanje + Otvori izbornik zapisa prilikom dodavanja u biblioteku + Veze + Prikaži vrijeme + Korisničko ime + Lozinka + Ručno instaliranje + Prijedlozi + Discord + Bogata prisutnost Discorda + Enable Discord Rich Presence + Prikaži gumbove + Prikaži gumb za preuzimanje + Duga + Zalazak sunca + OutRun + Spektralno + LCD + %1$s %2$s + Koristi: + Čitaj na %s + Pop + Pomakni se skroz dolje + Informacije o mangi + 6 sati + Aktualiziranje je završeno + Mirovanje + Ne smetaj + Prikaz Napretka + Traži izvore + Pomakni dolje + Sadržaj + Vlastita tema + Opušteno + Ručno podešavanje + Automatsko aktualiziranje programa + Open + Ažuriraj + Matrix + Cotton Candy + Doom + Mocha + Sapphire diff --git a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml index 1c362be11..065de3308 100644 --- a/i18n-kmk/src/commonMain/moko-resources/in/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/in/strings.xml @@ -68,7 +68,7 @@ Release page Don\'t auto download update Hapus entri asli dari perpustakaan? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Sembunyikan Tampilkan kategori tersembunyi Penutup panorama @@ -192,4 +192,7 @@ Menampilkan tombol untuk mengunduh aplikasi Tampilkan Tombol Discord Menampilkan tombol untuk bergabung dengan server Discord + WebDAV + Nama Pengguna + Kata sandi diff --git a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml index b1f921db7..2b621b810 100644 --- a/i18n-kmk/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/it/strings.xml @@ -116,7 +116,7 @@ Copia la pagina combinata negli appunti Non scaricare automaticamente l\'aggiornamento Rimuovere le voci originali dalla libreria? - Supporto: MangaDex, E-H, ExH, nH, 8Muse, Tsumino + Supporto: MangaDex, E-H, ExH, nH, 8Muse Patagonia Pop Superare @@ -198,4 +198,11 @@ Regolazione automatica per la visualizzazione su schermo panoramico Apri il menu tracciamento quando aggiungi una voce alla libreria Naviga + Pizzica per ingrandire + Utilizzando: + RPC Incognito + Scorrimento attraverso + Messaggiare con + Recupera automaticamente i metadati di un nuovo manga quando lo aggiungi alla libreria + Recupera automaticamente l'elenco dei capitoli di un nuovo manga quando lo aggiungi alla libreria diff --git a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml index 3d525c496..048c4c69f 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ja/strings.xml @@ -116,7 +116,7 @@ 結合ページをクリップボードにコピー 更新プログラムを自動的にダウンロードしない ライブラリから元の作品を削除しますか? - サポート: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + サポート: MangaDex, E-H, ExH, nH, 8Muses フォルダーを開く フォルダーを開けませんでした 16:9 @@ -148,4 +148,42 @@ Discord 今後も続々登場予定 Discord 本Discordサービスは、トークンを記録することで動作します。利用は自己責任でお願いします。トークンは端末内に保存され、%s が外部と共有することはありません。 + 含まれるカテゴリのエントリはDiscordに表示されません + Discordアカウント + アカウントが見つかりません + 閲覧 + ふざけてる人 + 進行状況を表示 + スクロール中 + Outrun + あなたが見ているエピソードのタイトルを表示する + Discord ステータス + ログインに失敗しました + Discordのカスタマイズ + カスタムメッセージ + Discordに表示されるカスタムメッセージ + 時間を表示 + ボタンを表示 + ダウンロードボタンを表示 + アプリをダウンロードするボタンを表示します + Rainbow + Discord Rich Presenceを有効にする + 🌙 離席中 + 🟢 オンライン + RPC Incognito + シークレットモードが有効になっているときにオーバーライドする + アプリをダウンロードしたりDiscordサーバーに参加するボタンを表示します + Discordシークレットモード + Spectral + Springfield + Discord Rich Presence + アクティブなアカウント + ⛔ マナーモード + スポンサーになりませんか + WebDAV + ユーザー名 + パスワード + %1$s %2$s + 使用: + %s で読む diff --git a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml index 9f12eb296..92d48563a 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ko/strings.xml @@ -64,4 +64,100 @@ 업데이트 중 배너 표시 소스 아이콘 언어 아이콘 사용 + If set to \"manual\", either refresh each manga or using the \"Invalidate downloads index\" action in \"Advanced\" + Auto sync progress from Trackers + Sync progress from Trackers up to chapter %d + Show syncing progress banner + Preload library\'s cover color + Access media files permission + Permission granted. Try action again. + Coming (soon) updates! + Show source-website\'s Suggestions while viewing entry + Scroll to bottom + To be updated + Pop + Monochrome + Skip it + Neutral + Tonal Spot + Expressive + Refresh extension page to update list. + More extensions... + Download cache renew interval + Manual + Open + Could not install update + Release page + Clean invalid downloads + Invalid downloads cleaned + Avoid using Private Installer because it\'s not stable yet, will lead to really slow start/resume App. + Saved Searches & Feeds + Auto-update app + Allow it + Allow all + Change Logs + Suggestions + Saved Searches (Hold to delete) + Library update errors + Library update errors (%d) + You have no library update errors. + Scroll to top + Rainbow + Fruit Salad + Fidelity + Casual + Sunset + Outrun + Raspberry + Cathode + LCD + Allow saving covers & using custom covers in entry\'s Edit Info + What\'s coming (soon) + Content + Bubblegum + Patagonia + Springfield + PICO-8 + Show restoring progress banner + Current: %1$s + Scroll to previous + Sort feeds + Would you like to sort the feeds alphabetically? + Don\'t auto download update + Night at the beach + Spectral + Find and remove all downloads, files, folders which are not saved in your library + Allow using custom covers in entry\'s Edit Info + Website Suggestions + More like these... + Saved Searches (Hold to add to Feed) + Scroll to next + Too many sources in your feed, cannot add more than 20 + Skip all + Try now + Update completed + Manual download + Vibrant + 서재에 추가할 때 동기화 메뉴 열기 + 디스코드 + 디스코드 리치 프레즌스 + 유휴 + 온라인 + %1$s %2$s + 맞춤 메시지 + 시간 표시 + Discord 리치 프레즌스 기능 켜기 + WebDAV + 사용자 이름 + 비밀번호 + 후원하기 + 버튼들 보이기 + 다운로드 단추 표시 + 업데이트 중 + 사용 중: + 검색 + 방해 금지 + 연결 + 명령어로 설치하기 + 진행 상황 표시 diff --git a/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml index 278f2dc1e..09241b1b3 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ms/strings.xml @@ -1,4 +1,46 @@ Cari sumber + 1 jam + 2 Jam + 12 jam + 1 Hari + Kata Laluan + Auto-kemas kini apl + Langkau semua + Pengesyoran + Dayakan Discord Rich Presence + Content + Bergerak ke bawah + Tema Kustom + Sembunyi + Neutral + Bergerak ke atas + Soalan Lazim dan Panduan + azab + Buka folder + Kemas kini + Monokrom + nilam + Mengemaskini + Manual + Nama pengguna + kemas kini siap + Open + Hubungan + %1$s %2$s + Baca terus %s + Cubit untuk zum + pelangi + Cloudflare + Matriks + Rasberi + Halaman keluaran + Tidak dapat memasang kemas kini + Discord + Tunjukkan butang muat turun + Discord Rich Presence + Jangan ganggu + Terbiar + Di talian diff --git a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml index ce9d4e2c2..f78ed82af 100644 --- a/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/nl/strings.xml @@ -115,5 +115,59 @@ Kopieer gecombineerde pagina naar klembord Update niet automatisch downloaden Originele invoergegevens uit bibliotheek verwijderen? - Ondersteuning: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Ondersteuning: MangaDex, E-H, ExH, nH, 8Muses + Gebruikersnaam + Map openen + Updaten + Bezig met update + WebDAV + Wachtwoord + %1$s %2$s + met: + Knijp om te zoomen + 16:9 + 3:2 + Hand­matige installatie + Discord + Deze Discord Service werkt met token loggen. Gebruik op eigen risico. Je token wordt lokaal opgeslagen en Anikku deelt het nergens anders + 🟢 Online + RPC Incognito + Toont een knop om de app te downloaden + Toon Discord knop + Messing rond met + Discord aanpassingen + 🌙 Idle + Auto-update app + Toon de titel van de aflevering die je bekijkt + Discord Rich Presence inschakelen + Discord status + ⛔ Niet storen + Invoer in opgenomen categorieën zal niet worden weergegeven in Discord + Discord accounts + Browsen + Aangepast bericht dat zal worden getoond op Discord + Toon voortgang + Toon tijd + Knoppen weergeven + Toon knoppen om de app te downloaden of toe te treden tot de Discord server + Downloadknop weergeven + Toont een knop om toe te treden tot de Discord server + Toon de omslag uitgelijnd aan de bovenkant naast anime info + Outrun + Pop + Bubblegum + Springfield + Scrollen door + Aangepast bericht + Speciale diensten + Discord, meer te komen.. + Discord Incognito modus + Overschreven wanneer Incognito modus is ingeschakeld + Geen accounts gevonden + Inloggen mislukt + Mocha + Sapphire + Actieve account + Verbindingen + Discord Rich Presence diff --git a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml index c1397db18..6afdebea4 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pl/strings.xml @@ -114,11 +114,63 @@ Kopiuj połączoną stronę do schowka Nie pobieraj automatycznie aktualizacji Usunąć oryginalne wpisy z biblioteki? - Wsparcie: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Wsparcie: MangaDex, E-H, ExH, nH, 8Muses Fatum Ręczna instalacja Mokka Szafir Patagonia Springfield + Nazwa użytkownika + Hasło + Otwórz folder + 3:2 + WebDAV + Nie udało się otworzyć folderu. + 16:9 + 4:3 + Aktualizowanie + %1$s %2$s + Wykorzystując: + Wesprzyj mnie + Otwórz menu po dodaniu do biblioteki + Aktualizacja + Ta usługa Discorda działa poprzez rejestrowanie tokenów. Używaj na własne ryzyko. Twój token jest przechowywany lokalnie, a Anikku nigdzie indziej nie udostępnia. + Wpisy w dołączonych kategoriach nie będą wyświetlane w Discordzie + Konta Discorda + Aktywne konto + Logowanie nie powiodło się + Niestandardowa wiadomość, która będzie wyświetlana na Discordzie + Pokaż postęp + Pokaż przyciski + Wyświetla przycisk do pobrania aplikacji + Pokaż przycisk Discorda + 🟢 Online + Fidelity + Usługi specjalne + Połączenia + Status Discorda + ⛔ Nie przeszkadzać + Wiadomość niestandardowa + Pokaż tytuł oglądanego odcinka + Włącz Discord Rich Presence + 🌙 Bezczynny + RPC Incognito + Tryb Discord Incognito + Zastąpione, gdy włączony jest tryb incognito + Wyświetlaj przyciski, aby pobrać aplikację lub dołączyć do serwera Discorda + Pokaż przycisk Pobierz + Wyświetla przycisk dołączenia do serwera Discorda + Pokaż okładkę wyrównaną do góry obok informacji anime + Outrun + Nie znaleziono kont + Przeglądanie + Przewijanie przez + Komunikacja z + Discord, jeszcze więcej. + Discord + Wzbogacona obecność Discorda + Pokaż czas + Pop + Dostosowanie Discorda diff --git a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml index 73ed54717..6061a4837 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -118,7 +118,7 @@ Copiar combinação de páginas para a área de transferência Não baixar a atualização automaticamente Remover entradas originais da biblioteca? - Suporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Suporte: MangaDex, E-H, ExH, nH, 8Muses Atualizar 16:9 20:9 @@ -147,4 +147,45 @@ Casual Outrun Abrir menu \'Acompanhar\' ao adicionar à biblioteca + Este serviço do Discord funciona através do registro de tokens. Use por sua conta e risco. Seu token é armazenado localmente e o Anikku não o compartilha em nenhum outro lugar + Exibir o título do episódio que você está assistindo + Habilitar o Rich Presence do Discord + Status do Discord + Substituído quando o Modo Anônimo está ativado + As entradas nas categorias incluídas não serão exibidas no Discord + Contas do Discord + Nenhuma conta encontrada + Falha ao logar + Exibe um botão para entrar no servidor do Discord + RPC Incognito + Navegação + Rolagem através + Mostrar progresso + Mostrar hora + Mostrar botões + Exibir botões para baixar o aplicativo ou entrar no servidor do Discord + Mostrar botão do Discord + Serviços Especiais + Conexões + Discord, mais para vir... + Personalização do Discord + Mensagem personalizada que será exibida no Discord + Mostrar botão de download + Exibe um botão para baixar o aplicativo + Rich Presence do Discord + Modo incógnito do Discord + Conta ativa + Envia mensagens com + Mensagem personalizada + Discord + ⛔ Não perturbe + 🌙 Idle + 🟢 Online + %1$s %2$s + Leia em %s + WebDAV + Nome de usuário + Senha + Uso + Tornar-se patrocinador diff --git a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml index 9435f1e1e..b66038182 100644 --- a/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/pt/strings.xml @@ -118,7 +118,7 @@ Copiar página combinada para área de transferência Não baixar a atualização automaticamente Remover entradas originais da biblioteca? - Suporte: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Suporte: MangaDex, E-H, ExH, nH, 8Muses Instalação manual Abrir pasta Limpar mangá @@ -192,4 +192,6 @@ Exibe um botão para baixar o aplicativo Mostrar botão do Discord Exibe um botão para entrar no servidor do Discord + Nome de Utilizador + Palavra passe diff --git a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml index 4187dc493..1291a4f62 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ro/strings.xml @@ -115,5 +115,57 @@ Copiază pagina combinată în clipboard Nu descărca automat Eliminaţi intrările originale din bibliotecă? - Suport: MangaDex, E-H, ExH, nH, 8Muse, Tsumino + Suport: MangaDex, E-H, ExH, nH, 8Muse + Servicii speciale + Conexiuni + Cont activ + Mesaje în jur cu + Arată butonul de Descărcare + Afișează un buton pentru descărcarea aplicației + Arată coperta aliniată în partea de sus alături de informații anime + Afișează butoane pentru a descărca aplicația sau pentru a se alătura serverului Discord + Mocha + Patagonia + Discord, mai multe de făcut. + Arată butoanele + Prezență Discord Rich + Arată titlul episodului pe care îl urmăriți + ⛔ Nu deranja + 🟢 Online + Modul Incognito Discord + Suprascrie când modul Incognito este activat + Nici un cont găsit + Autentificare eșuată + Defilare prin + Personalizare Discord + Mesaj personalizat + Mesaj personalizat care va fi afișat pe Discord + Arată timpul + Vibrant + Sapphire + Outrun + Pop + Springfield + Navigare + Activează Discord Rich Presence + 🌙 Idle + Intrările în categoriile incluse nu vor fi afișate în Discord + Conturi Discord + RPC Incognito + Discord + Acest serviciu Discord funcționează prin conectarea simbolurilor. Folosiți pe propriul risc. Token-ul dvs. este stocat local și %s nu îl partajează nicăieri altundeva + Arată progresul + Stare Discord + Arată butonul Discord + Afișează un buton pentru a se alătura serverului Discord + Se actualizează + 3:2 + WebDAV + Nume de utilizator + Parolă + Instalare manuală + %1$s %2$s + Folosind: + Deschide Folder + Actualizați diff --git a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml index 86bbcd3c5..e3b8fb9a4 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ru/strings.xml @@ -119,7 +119,7 @@ Скопировать объединенную страницу в буфер обмена Не скачивать автоматически Удалить оригинальные тайтлы из библиотеки? - Поддержка: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Поддержка: MangaDex, E-H, ExH, nH, 8Muses Выровнять обложку к верху Показать обложку выровненной к вершине рядом с информацией о манге Дум diff --git a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml index 7596a3375..bebcf5ce5 100644 --- a/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/sv/strings.xml @@ -116,6 +116,58 @@ Kopiera kombinerad sida till urklipp Uppdatera inte automatisk nedladdning Ta bort ursprungliga poster från biblioteket? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Manuell installation + Rullar igenom + Ringer runt med + Visa knappar för att ladda ner appen eller gå med i Discord-servern + Springfield + Discord, mer att komma.. + Denna Discord-tjänst fungerar genom tokenloggning. Använd på egen risk. Din token lagras lokalt och Anikku delar den inte någon annanstans + Särskilda tjänster + Poster i inkluderade kategorier kommer inte att visas i Discord + Anpassat meddelande + Anpassat meddelande som kommer att visas på Discord + Visa framsteg + Aktivera Discord Rich Presence + ⛔ Stör ej + Visa Discord-knapp + Visar en knapp för att ansluta till Discord-servern + Discord Rich Presence + Visa titeln på avsnittet du tittar på + Fidelity + Visa omslaget justerat till toppen tillsammans med anime info + Outrun + Anslutningar + Discord + Discord inkognitoläge + Åsidosätt när inkognitoläge är aktiverat + Surfning + 🌙 Idle + 🟢 Online + Discords anpassning + Visa knappar + Visa nedladdningsknapp + Visar en knapp för att ladda ner appen + Mocha + Pop + Aktivt konto + Sapphire + Discords status + RPC Incognito + Discord-konton + Inga konton hittades + Kunde inte logga in + Visa tid + Öppna mapp + 3: 2 + WebDAV + Användarnamn + Lösenord + Nyp för att zooma + 16:9 + %1$s %2$s + Använder: + Uppdaterar + Uppdatera diff --git a/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml b/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml index 310f93f9a..3a9549b8c 100644 --- a/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/ta/strings.xml @@ -1,4 +1,77 @@ உங்கள் ஊட்டத்தில் பல ஆதாரங்கள், 20 க்கு மேல் சேர்க்க முடியாது + மேலும் நீட்டிப்புகள் + கையேடு + 1 மணி நேரம் + 2 மணி நேரம் + 6 மணி நேரம் + 12 மணி நேரம் + 1 நாள் + கடவுச்சொல் + இப்போது முயற்சிக்கவும் + புதுப்பிப்பு முடிந்தது + மேலே உருட்டவும் + செயலில் கணக்கு + பயன்படுத்துகிறது: + %s படிக்கவும் + தனிப்பயன் செய்தி + ஆதாரங்களைத் தேடுங்கள் + மேலே செல்லுங்கள் + கீழே செல்லுங்கள் + மறை + திறந்த கோப்புறை + புதுப்பிப்பு + நேரத்தைக் காட்டு + Webdav + பயனாளர் பெயர் + அனைத்தையும் அனுமதிக்கவும் + அனைத்தையும் தவிர்க்கவும் + கைமுறை நிறுவல் + கைமுறையாக பதிவிறக்கம் + பரிந்துரைகள் + கீழே உருட்டவும் + நடப்பு: %1$s + முரண்பாடு பணக்கார இருப்பு + நிலையிக்கம் + கணக்குகள் எதுவும் இல்லை + %1$s %2$s + உலாவுதல் + பொத்தான்களைக் காட்டு + கோப்புறையைத் திறக்கத் தவறிவிட்டது. + பெரிதாக்க கிள்ளுதல் + 16:9 + 3:2 + மங்கா செய்தி + டோனல் ச்பாட் + நடுநிலை + துடிப்பான + வெளிப்படுத்தும் + ரெயின்போ + பழ சாலட் + ஒரே வண்ணமுடைய + மெய்நிலை + உள்ளடக்கம் + கிளவுட்ஃப்ளேர் + பருத்தி மிட்டாய் + டூம் + அணி + மோச்சா + சபையர் + Custom Theme + கருக்கல் + விஞ்சி + புற்றுப்பழம் + பாப் + நிறமாலை + எல்.சி.டி. + புதுப்பித்தல் + நூலகத்தில் சேர்ப்பதில் டிராக் மெனுவைத் திறக்கவும் + முரண்பாடு பணக்கார இருப்பை இயக்கவும் + முன்னேற்றத்தைக் காட்டு + இணைப்புகள் + முரண்பாடு + தொந்தரவு செய்ய வேண்டாம் + ஆன்லைனில் + திற diff --git a/i18n-kmk/src/commonMain/moko-resources/th/strings.xml b/i18n-kmk/src/commonMain/moko-resources/th/strings.xml index 55344e519..37638f8ff 100644 --- a/i18n-kmk/src/commonMain/moko-resources/th/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/th/strings.xml @@ -1,3 +1,60 @@ - \ No newline at end of file + ด้วยตนเอง + 1 ชั่วโมง + 2 ชั่วโมง + 6 ชั่วโมง + WebDAV + อนุญาตทั้งหมด + อัปเดตเสร็จแล้ว + เลื่อนกลับสู่ด้านบน + พร้อมใช้งาน + %1$s %2$s + แสดงปุ่มดาวน์โหลด + ราสเบอร์รี่ + ขาวดำ + 12 ชั่วโมง + แสดงออก + เป็นกลาง + เลื่อนขึ้น + อัปเดต + คำแนะนำ + อายุขัย + คำถามที่พบบ่อยและคู่มือ + ซ่อน + แสดงหมวดหมู่ที่ซ่อนอยู่ + เปิดแฟ้ม + 4:3 + ที่กําหนดเอง ธีม + แคชชวล + อัปเดตแอปอัตโนมัติ + ข้ามทั้งหมด + Discord + ชื่อผู้ใช้ + รหัสผ่าน + เปิด + หน้ารีลีส + ไม่สามารถติดตั้งอัปเดต + การเชื่อมต่อ + กำลังเปิด + แสดงความคืบหน้า + แสดงเวลา + หดเพื่อซูม + 16:9 + 3:2 + ข้อมูลมังงะ + จุดวรรณยุกต์ + ความสดใส + สีรุ้ง + สลัดผลไม้ + ความจงรักภักดี + เนื้อหา + ปาตาโกเนีย + สปริงฟิลด์ + 1 วัน + ห้ามรบกวน + ออนไลน์ + ดิสครอด Rich Presence + เลื่อนลง + กำลังอัปเดต + diff --git a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml index 47e52c143..3a2ed7033 100644 --- a/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/tr/strings.xml @@ -100,9 +100,55 @@ Release page Otomatik indirmeyi devre dışı bırak Remove original entries from library? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses Uygulamayı otomatik güncelle Kütüphane güncelleme hataları (%d) Kütüphane güncelleme hataları Manuel kurulum + İlerlemeyi takip servisleriyle otomatik eşitle + Sync progress from Trackers up to chapter %d + Update completed + You have no library update errors. + Panorama cover + Copy first page to clipboard + Copy combined page to clipboard + Panorama Cover + Scroll to previous + Could not install update + Scroll to bottom + Copy second page to clipboard + Show cover in landscape mode if it\'s a wide image + Panorama Cover Flow + Show panorama cover in Global Search & Suggestions + Mocha + Güncelleniyor + To be updated + Panorama comfortable grid + Pop + Kitaplığa eklerken izleyici menüsünü aç + Discord Detaylı Etkinlik + Saati Göster + Güncelleme + Yakınlaştırmak için sıkıştır + WebDAV + Kullanıcı adı + Parola + Discord + Discord etkinliğini etkinleştir + Eylemsiz + Çevrimiçi + Hesap yok + %1$s %2$s + Şunu kullanarak: + Gözatma + %s üzerinde oku + İlerlemeyi Göster + Düğmeleri Göster + 16:9 + 4:3 + 3:2 + Bağlantılar + Klasörü aç + Rahatsız Etme + İndirme butonunu göster diff --git a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml index fe28b796d..dafaf0bfe 100644 --- a/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/uk/strings.xml @@ -118,7 +118,7 @@ Скопіювати комбіновану сторінку в буфер обміну Не завантажувати автоматично оновлення Видалити оригінальні записи з бібліотеки? - Підтримка: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Підтримка: MangaDex, E-H, ExH, nH, 8Muses Відкрити папку Очистити мангу Оновити @@ -137,4 +137,48 @@ Підтримати мене Аутран Springfield + Цей сервіс Discord працює через систему з використанням жетонів. Використовуйте на ваш власний ризик. Ваш токен зберігається на локальному і Anikku не поширює його ніде інше + Rich Presence Discord + Увімкнути Rich Presence в Discord + Статус Discord + RPC Incognito + Категорії у базі даних не будуть відображатися в Discord + Облікові записи Discord + Активний обліковий запис + Рахунків не знайдено + Прокрутка + Обмін інформацією з + Показувати кнопку завантаження + Відображає кнопку для приєднання до серверу Discord + Перезаписувати, коли ввімкнено режим інкогніто + 🌙 Очікування + Не вдалося увійти + Користувацьке повідомлення + Discord, ще більше... + Спеціальні послуги + Показати назву епізоду, що ви дивитеся + ⛔ Не турбувати + Перегляд + З\'єднання + Налаштування Discord + Користувацькі повідомлення, які будуть відображатися в Discord + Відображати час + Показати кнопки + Показувати кнопки для завантаження програми або приєднання до сервера Discord + Відображає кнопку для завантаження програми + Показувати кнопку Discord + 🟢 Online + Режим інкогніто Discord + Показувати поступ + Discord + 16:9 + Використовує: + 4:3 + %1$s %2$s + WebDAV + Ім\'я користувача + Пароль + Прищемити, щоб масштабувати + 3:2 + Читати на %s diff --git a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml index cc02a045a..d1b6dcb78 100644 --- a/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/vi/strings.xml @@ -122,7 +122,7 @@ Sao chép trang đã kết hợp vào clipboard Không tự động tải xuống bản cập nhật Xóa các truyện gốc ra khỏi thư viện? - Hỗ trợ: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Hỗ trợ: MangaDex, E-H, ExH, nH, 8Muses Cài đặt thủ công Mở thư mục Xóa manga @@ -137,4 +137,25 @@ Dữ liệu đã tải xuống Chương & Lịch sử từ cơ sở dữ liệu Tài trợ cho tôi + Kéo để phóng to + Kết nối + Discord + Tích hợp trạng thái Discord + Kích hoạt tính năng Discord Rich Presence + Chế độ không làm phiền + Đang chờ + Trực tuyến + Không có tài khoản nào + %1$s %2$s + Dùng: + Đang duyệt + Đọc trên %s + Hiển thị thời gian + Hiện nút tải xuống + 16:9 + 4:3 + 3:2 + WebDAV + Tên người dùng + Mật khẩu diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml index 5758f29a2..777c38ac8 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -122,11 +122,59 @@ 复制合并页面到剪贴板 不自动下载更新 从书架删除原始条目? - Support: MangaDex, E-H, ExH, nH, 8Muses, Tsumino + Support: MangaDex, E-H, ExH, nH, 8Muses 打开文件夹 打开文件夹时出错 手动安装 赞助 缩减点按区域 更新 + 用户名 + 密码 + 添加到仓库时打开进度记录菜单 + 捏合放大 + 16:9 + 4:3 + 3:2 + WebDAV + %1$s %2$s + 在 %s 查看 + 正在使用: + 数据库中的剧集与观看记录 + Discord 状态 + 显示加入Discord服务器的按钮 + 连接 + 🌙 空闲的 + 显示按钮下载应用程序或加入Discord服务器 + 显示时间 + 更新 + RPC Incognito + 特别服务 + Discord + 这个Discord服务通过令牌记录来运行。请自行承担风险。您的令牌是本地存储的,%s 不在任何其他地方共享。 + Discord 丰富存在 + 显示您观看的剧集的标题 + 启用 Discord Rich Presence + Discord 隐身模式 + 包含类别中的条目将不会在Discord中显示 + 登录失败 + 四周消息: + 在 Discord 上显示的自定义消息 + 显示进度 + 显示 Discord 按钮 + 与动画信息一起显示顶部对齐的封面 + Discord,更多即将到来。 + 当隐身模式启用时覆盖 + 未找到账户 + Discord 自定义 + 显示按钮 + 显示下载按钮 + ⛔ 勿扰模式 + 🟢 Online + 显示下载应用程序的按钮 + 自定义消息 + 浏览中 + 滚动通过 + Discord 账户 + 活动账户 diff --git a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml index 53fbe39e0..928b26e66 100644 --- a/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -122,7 +122,7 @@ 將雙頁複製到剪貼簿 不自動下載更新 要從書櫃移除原始條目嗎? - 支援:MangaDex、E-H、ExH、nH、8Muses、Tsumino + 支援:MangaDex、E-H、ExH、nH、8Muses 手動下載 將封面與頂部對齊 將封面與頂部對齊並顯示漫畫資訊 diff --git a/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml b/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml index fa0582be1..c3d3f6dde 100644 --- a/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml +++ b/i18n-sy/src/commonMain/moko-resources/ar/plurals.xml @@ -91,10 +91,10 @@ وقت الإنغلاق ‬وقت الإغلاق - - - - + وقتان للإغلاق + %1$d أوقات للإغلاق + %1$d وقتًا للإغلاق + %1$d وقت للإغلاق ترحيل ‫ المدخل؟ diff --git a/i18n-sy/src/commonMain/moko-resources/ar/strings.xml b/i18n-sy/src/commonMain/moko-resources/ar/strings.xml index ae6f66665..361f89c45 100644 --- a/i18n-sy/src/commonMain/moko-resources/ar/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/ar/strings.xml @@ -215,7 +215,7 @@ استخدم شبكة Hentai@Home يتم حاليًا عرض العناوين الإنجليزية/الحروف اللاتينية في نتائج البحث. امسح ذاكرة التخزين المؤقت للفصل بعد تغيير هذا (في القسم المتقدم) فئاتExHentai التي شاهدتها - يجب ان تكون القيمة بين صفر و ٩٩٩٩‐ + يجب أن تكون القيمة بين -٩٩٩٩ و ٠! سيتم تضمين المعارض التي تم تحميلها مؤخرًا على شاشة المشاهدة إذا كانت تحتوي على علامة مشاهدة واحدة على الأقل ذات وزن إيجابي، ويصل مجموع الأوزان على العلامات التي تمت مشاهدتها إلى هذه القيمة أو أعلى. يمكن تعيين هذه العتبة بين ٠ و٩٩٩٩. حاليًا القيمة :%1$d يجب ان تكون القيمة بين صفر و ٩٩٩٩ ! إذا كنت ترغب في إخفاء المعارض التى بلغات معينة من قائمة المعرض والبحث، اختارها في الحوار الذى سوف يظهر. @@ -245,16 +245,7 @@ عرض إحصائيات التحديث جارٍ جمع الإحصائيات… إحصائيات تحديث المعرض - " -\nالمعارض التي تم فحصها في الماضي -\n- ساعة : %1$d -\n-٦ ساعات : %2$d -\n-١٢ ساعة : %3$d -\n-يوم : %4$d -\n-يومين: %5$d -\n-أسبوع : %6$d -\n-شهر : %7$d -\n-سنة : %8$d" + \nالمعارض التي تم فحصها في الماضي\n\n- ساعة : %1$d\n\n-٦ ساعات : %2$d\n\n-١٢ ساعة : %3$d\n\n-يوم : %4$d\n\n-يومين: %5$d\n\n-أسبوع : %6$d\n\n-شهر : %7$d\n\n-سنة : %8$d تخطي إعادة تصميم الصفحة تحميل الإعدادات إلى الخادم حدث خطأ أثناء عملية التكوين:%1$s @@ -538,7 +529,7 @@ ملاحظات مزامنة المفضلة الهامة إنهاء \'%1$s\'%2$s - رائع + مدهش قصة بديلة عناوين متشابهة مصدر MangaDex المفضل @@ -575,7 +566,7 @@ ادخل إلى مؤلم لا تقييم - ‬الفنان + فنان رسومات حاسوبية النوع خطأ في تنزيل الفصول اندماج المراجع غير صالح @@ -636,10 +627,16 @@ مجموعات المسح للعرض تدور خارج دوجينشي - Non-H + غير إباحي إباحيات آسياوية المُعرّف is Exhentai gallery %1$s مترجم دوجينشي + اسم الملف + امتداد الملف + عنوان URL الأساسي + الفصل الأخير + تضمين تجزئة عنوان URL للفصل + أضف الأحرف الستة الأولى من تجزئة MD5 الخاصة بعنوان URL الخاص بالفصل إلى اسم ملف أو مجلد الفصل. diff --git a/i18n-sy/src/commonMain/moko-resources/es/strings.xml b/i18n-sy/src/commonMain/moko-resources/es/strings.xml index 3de10f48c..2c2a0c77c 100644 --- a/i18n-sy/src/commonMain/moko-resources/es/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/es/strings.xml @@ -1,6 +1,6 @@ - Buscar manualmente + Búsqueda manual Migrar ahora Limpiar títulos Editar información @@ -19,7 +19,7 @@ Solo clientes de puerto predeterminado Mostrar títulos japoneses en los resultados de búsqueda ¡Debe estar entre -9999 y 0! - ¡Debe estar entre 0 y 9999! + Escribe un número entre 0 y 9999 1280x Exploración E/ExHentai mejorada No migrar @@ -28,7 +28,7 @@ Webtoon Manga Todas las fuentes - Ajustes de Bifurcación + Ajustes de esta versión de la aplicación Use la red Hentai@Home Desea cargar imágenes a través de la red de Hentai@Home, si está disponible? Deshabilitar esta opción reducirá la cantidad de páginas que puede ver \nOpciones: @@ -39,27 +39,25 @@ Utiliza imágenes originales Actualmente usando imágenes originales Etiquetas vistas - Filtrado de idioma + Filtrar idiomas 1600x Categorías de la página principal 2400x La calidad de las imágenes descargadas Calidad de imagen - Auto + Automática 980x Desactivar la subida de favoritos 780x Actualmente se muestran los títulos en inglés/romanizado en los resultados de búsqueda. Borra la caché de capítulos después de cambiar esto (en la sección Avanzado) Abre una vista web a tu página de etiquetas vistas de E/ExHentai - ¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? Aún pueden habilitarse activando sus filtros - Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, selecciónalas en el cuadro de diálogo que aparecerá. -\nTen en cuenta que las galerías coincidentes nunca aparecerán, sin importar tu consulta de búsqueda. -\nTldr marcado = excluir + ¿Qué categorías te gustaría mostrar por defecto en la página principal y en las búsquedas? El resto se puede seguir viendo activando filtros + Si deseas ocultar galerías en ciertos idiomas de la lista de galerías y búsquedas, márcalas a continuación. \nTen en cuenta que las galerías que marcadas nunca aparecerán, aunque las busques. \nEn pocas palabras: si está marcado = se excluye Etiquetas Observadas en ExHentai Umbral de Filtrado de Etiquetas - Umbral de Monitoreo de Etiquetas + Umbral de monitoreo de etiquetas Las galerías recientemente subidas se incluirán en la pantalla de observación si tienen al menos una etiqueta observada con peso positivo, y la suma de los pesos de sus etiquetas observadas alcanza este valor o es mayor. Este umbral se puede establecer entre 0 y 9999. Actualmente: %1$d - Estado Predeterminado del Filtro de la Lista Observada + Estado predeterminado del filtro de la lista de favoritos Al navegar en ExHentai/E-Hentai, ¿debería estar habilitado el filtro de la lista de seguimiento de forma predeterminada? Habilitar/Deshabilitar el menú de navegación mejorado hecho para E/ExHentai Sincronización de Favoritos de E-Hentai @@ -67,15 +65,15 @@ Los favoritos solo se descargan desde ExHentai. Cualquier cambio en los favoritos en la aplicación no se cargará. Previene la pérdida accidental de favoritos en ExHentai. Ten en cuenta que las eliminaciones aún se descargarán (si eliminas un favorito en ExHentai, también se eliminará en la aplicación). Mostrar notas sincronizadas de favoritos Mostrar información sobre la función de sincronización de favoritos - ¡Por favor inicia sesión! + ¡Tienes que iniciar sesión! Ignorar errores de sincronización cuando sea posible - Forzar el restablecimiento del estado de sincronización - Restablecimiento del estado de sincronización + Restablece el estado de sincronización + Restablecer estado de sincronización No canceles el proceso de sincronización inmediatamente si encuentras errores. Los errores se seguirán mostrando cuando se complete la sincronización. En algunos casos, puede provocar la pérdida de favoritos. Resulta útil cuando se sincronizan bibliotecas grandes. Realiza una resincronización completa en la próxima sincronización. Las eliminaciones no se sincronizarán. Todos los favoritos de la aplicación se volverán a cargar en ExHentai y todos los favoritos de ExHentai se volverán a descargar en la aplicación. Útil para reparar la sincronización después de que se haya interrumpido. - Comprobador de actualizaciones de la galería - Restricciones para actualizaciones automáticas - Tiempo entre actualizaciones + Comprobar actualizaciones de la galería + Restringir actualizaciones automáticas + Intervalo de tiempo entre actualizaciones Nunca actualizar las galerías 1 hora 2 horas @@ -91,21 +89,19 @@ El actualizador ejecutó %1$s por última vez, y comprobó %2$d de las %3$d galerías que estaban listas para ser comprobadas. El actualizador aún no se ha ejecutado. Nota de perfil de configuración - ¡Ajustes cargados correctamente! - ¡Error en la configuración! + ¡Los ajustes se han subido! + ¡Hubo un error en la configuración! Se ha producido un error durante el proceso de configuración: %1$s - Cargar la configuración en el servidor + Subiendo ajustes al servidor %1$s comprueba/actualiza las galerías por lotes. Esto significa que esperará %2$d hora(s), comprobará %3$d galerías, esperará %2$d hora(s), comprobará %3$d y así sucesivamente… - \nGalerías que se comprobaron en:\n- hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- día: %4$d\n- 2 días: %5$d\n- semana: %6$d\n- mes: %7$d\n- año: %8$d - La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios. -\n -\nSi no tienes ni idea de lo que son los perfiles de configuración, probablemente no importe, simplemente pulsa \"Aceptar\". - Por favor espere, esto puede tardar algún tiempo… - ¡No tienes espacios en tu perfil %1$s, por favor elimine un perfil! - Vuelva a verificar el estado de inicio de sesión + \nGalerías que se comprobaron hace:\n- una hora: %1$d\n- 6 horas: %2$d\n- 12 horas: %3$d\n- un día: %4$d\n- 2 días: %5$d\n- una semana: %6$d\n- un mes: %7$d\n- un año: %8$d + La aplicación añadirá ahora un nuevo perfil de configuración en E-Hentai y ExHentai para optimizar el rendimiento de la aplicación. Asegúrate de tener menos de tres perfiles en ambos sitios. \n \nSi no tienes ni idea de lo que son los perfiles de configuración no te preocupes por esto y pulsa «Aceptar». + Espera un momento, puede tardar un poco… + ¡No tienes espacio en tu perfil %1$s, primero elimina un perfil ya existente! + Recomprobar estado de inicio de sesión Página de inicio de sesión alternativa Saltar el cambio de estilo de la página - Cookie ígneo personalizado + Cookie «igneous» personalizada Algunos usuarios no pueden acceder a ExHentai de la manera normal y deben ingresar un valor específico de cookie ígneo. Esta opción es para esos usuarios. Herramientas de desarrollador Activar funciones integradas de hentai @@ -113,51 +109,49 @@ Activar fuentes delegadas Aplicar %1$s mejoras a las siguientes fuentes si están instaladas: %2$s Nivel de registro - Cambiar esto puede afectar el rendimiento de la aplicación. Reinicie la aplicación forzosamente después de realizar el cambio. Valor actual: %s + Cambiar esto puede afectar el rendimiento de la aplicación. Reinicia la aplicación manualmente tras el cambio. Valor actual: %s Activar lista negra de fuentes - Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicie la aplicación forzosamente después de realizar el cambio. + Ocultar extensiones/fuentes que son incompatibles con %1$s. Reinicia la aplicación manualmente tras el cambio. Abrir menú de depuración - ¡PUEDE CORROMPER SU BIBLIOTECA!]]> - Comenzando la limpieza + ¡PUEDES CORROMPER TU BIBLIOTECA!]]> + Comenzando el borrado Limpiar capítulos descargados - Eliminar carpetas de capítulos inexistentes, parcialmente descargadas y leídas - No hay carpetas para limpiar - Limpiar huérfanos - Limpiar leídos - Limpiar entradas no en la biblioteca + Elimina carpetas de capítulos inexistentes, parcialmente descargados, o ya leídos + No hay ninguna carpeta que limpiar + Borrar descargas huérfanas + Borrar ya leídos + Borrar lo que no esté en la biblioteca Ahorro de datos - Comprimir imágenes antes de descargarlas o cargarlas en el lector - Usar ahorrador de datos en el descargador + Comprimir imágenes antes de descargarlas o verlas en el visor + Descargar con el ahorrador de datos Ignorar imágenes JPEG Ignorar animaciones GIF Calidad de imagen - Comprimir a Jpeg - El tamaño del archivo Jpeg es considerablemente más pequeño que el de Webp (lo que significa que se guarda más datos), pero también hace que las imágenes pierdan más calidad. -\nActualmente, se comprime a Jpeg - Valores más altos significan que se guarda un mayor porcentaje de la calidad de la imagen, pero también implica que el tamaño del archivo es mayor; un 80 por ciento es una buena media entre el tamaño del archivo y la calidad de la imagen - El tamaño de; archivo JPEG es considerablemente mas pequeño que el WEBP (lo que significa que ser guardan mas datos), pero tambien hace que las imagenes pierdan mucha calidad. -\nActualmente se comprimen en WEBP + Comprimir como JPEG + El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando JPEG. + Al poner un porcentaje mayor aumenta la calidad de la imagen, pero también lo hace el tamaño en disco. Dejarlo en 80% es un término medio entre tamaño aceptable y con calidad. + El tamaño de un archivo JPEG es considerablemente más pequeño que el de WebP (lo que significa que te ahorras datos de descarga), pero también hace que las imágenes pierdan más calidad.\nAhora mismo se comprime utilizando WEBP. Convertir en Blanco & Negro Bandwidth Hero ( Requiere un servidor proxy ) wsrv.nl - Pom aqui la URl del Servidor Proxy de Bandwidth Hero + Pega aquí la dirección URL del servidor proxy de Bandwidth Hero Conservar obras con capítulos leídos Servidor Proxy de Bandwidth Hero - Minimo + Mínimo Extra Extremo - Solo errores criticos - Todos los registros - Modo de inspeccion de red + Solo errores críticos + Registrar todos los mensajes + Modo de inspección de red Ampliar todos los filtros de búsqueda de forma predeterminada - Vista previa del recuento de filas + Filas en vista previa Mostrar actualizaciones en la barra de navegación Mostrar historial en la barra de navegación Mostrar siempre las etiquetas de navegación Barra de navegación - Mostrar recomendaciones en el menú - Coloque el botón de recomendaciones en el menú adicional en lugar de en la página de entrada - Fusionar en el menú adicional + Recomendaciones en menú lateral + Poner el botón de recomendaciones en el menú lateral en vez de en la página de la obra + Fusionar en el menú lateral Coloque el botón de fusión en el menú adicional en lugar de en la página de entrada Ajustes de ordenación Utilizar las últimas preferencias y fuentes guardadas antes de la migración para migrar @@ -168,7 +162,7 @@ Marcar nuevos capítulos duplicados como leídos Marcar automáticamente nuevos capítulos como leídos si se han leído antes Cada hora - Ocultar pestaña Feed + Ocultar pestaña de novedades Filtrar las fuentes que están en categorías, haciendo que las fuentes no se pongan debajo del idioma si están en una categoría Reemplace el botón más reciente con una vista de exploración personalizada que incluya tanto lo más reciente como la exploración Todas las obras leídas @@ -177,8 +171,8 @@ Cada 30 minutos Filtrar las fuentes en categorías Cada 3 horas - Posición de la pestaña Feed - ¿Quieres que la pestaña feed sea la primera pestaña en navegar? Esto hará que sea la pestaña predeterminada al abrir la navegación, no se recomienda si está con datos móviles o una red medida + Posición de la pestaña de novedades + ¿Quieres que la pestaña de novedades sea la inicial? No te recomendamos que lo actives si tienes datos móviles o datos limitados Reemplazar el botón más reciente Carpetas ocultas de fuente local Permite a la fuente local leer carpetas ocultas @@ -217,10 +211,10 @@ Editar horarios de bloqueo No tienes horarios de bloqueo biométrico. Pulsa el botón «+» para añadir uno. ¡Una hora de bloqueo entra en conflicto con otra existente! - Introducir hora de inicio - Introducir hora de finalización + Introduce una hora de inicio + Introduce una hora de finalización Eliminar intervalo de tiempo - ¿Deseas eliminar el intervalo de tiempo %s? + ¿Quieres borrar el intervalo de tiempo %s? Días de bloqueo biométrico Días para tener la aplicación bloqueada Domingo @@ -242,7 +236,7 @@ Jueves Cifrar base de datos Viernes - Requiere reiniciar la aplicación para que surta efecto + Es necesario reiniciar la aplicación para que surta efecto ACTIVAR ESTO CREARÁ UNA NUEVA BASE DE DATOS. USA ESTOS PASOS PARA MANTENER TUS DATOS
1. AJUSTES -> COPIA DE SEGURIDAD -> CREAR
2. AJUSTES DEL SISTEMA -> BORRAR LOS DATOS DE LA APLICACIÓN
3. ABRIR LA APLICACIÓN Y ACTIVAR ESTO
4. AJUSTES DEL SISTEMA -> FORZAR REINICIO
5. AJUSTES -> COPIA DE SEGURIDAD -> RESTAURAR]]>
Descargas protegidas por contraseña Encripta las descargas de archivos CBZ con la contraseña dada.\nADVERTENCIA: LOS DATOS DE LOS ARCHIVOS SE PERDERÁN PARA SIEMPRE SI SE OLVIDA LA CONTRASEÑA @@ -435,7 +429,7 @@ Eliminando %1$d galerías del servidor remoto ¡No se pudo eliminar las galerías del servidor remoto! Estado de seguimiento - Sin seguir + Sin seguimiento Sincronizar favoritos de EH ¿Estás seguro? Restablecer el estado de sincronización puede hacer que la próxima sincronización sea extremadamente lenta. @@ -490,7 +484,7 @@ Derivado de Historia alternativa Versión alternativa - ¿Añadir %1$s al feed? + ¿Añadir %1$s a novedades? Error: %1$s No se pudo abrir esta entrada:\n\n%1$s Iniciando aplicación… @@ -549,11 +543,11 @@ Recortar el borde vertical hace unos instantes Recortar bordes Webtoon - Feed - ¿Borrar artículo de feed? + Novedades + ¿Borrar el elemento de novedades? ¡Lee esto! ¡Etiquetas deben ser exactas, no hay coincidencias parciales, no puedes hacer netorare para excluir mujer:netorare o similar!\nEl estilo para etiquetas de nombre es\n\"mujer: solo mujer\"\n¡sin citas!\n¡Se pueden añadir multiples variantes de la misma etiqueta, así que puedes hacer \"etiqueta:netorare\" para NHentai y \"mujer:netorare\" para E-Hentai! Selecciona ninguno - No tiene fuentes en su feed, navegar a la parte superior derecha para añadir una + No tienes ninguna fuente en novedades, añade alguna desde la parte superior derecha Saltar pre-migración Buscar parámetro (p. ej. idioma:inglés) Lascivo @@ -584,7 +578,7 @@ Número de páginas Padre Cargador - URL + Dirección URL Cargador principal Seguir estado %1$s TR @@ -610,7 +604,7 @@ Monocromo El título no tiene seguimiento. Elige un servicio de seguimiento - Rellenar desde el serv. de seguimiento + Rellenar desde seguim. No se puede añadir la galería al servidor remoto: \'%1$s\' (GID: %2$s)! La búsqueda no se ha podido completar Recomendaciones en común @@ -626,10 +620,13 @@ Recomendaciones de la fuente Servicios de recomendación en los que buscar Ocultar elementos que ya estén en la biblioteca - Encuentra el título automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex - Elegir títulos a través de los metadatos de la fuente + Encuentra la obra automáticamente si la fuente ya enlaza con el servicio de seguimiento. De momento solo funciona en MangaDex + Elegir obras a través de los metadatos de la fuente Escanear un código QR Capítulo final Extensión de archivo - URL base + Dirección URL base + Nombre de archivo + Añade las primeras seis letras del hash MD5 de la dirección URL del capítulo al nombre de archivo o carpeta local. + Incluir el hash de la URL del capítulo
diff --git a/i18n-sy/src/commonMain/moko-resources/fil/strings.xml b/i18n-sy/src/commonMain/moko-resources/fil/strings.xml index 0af9d883f..990096a14 100644 --- a/i18n-sy/src/commonMain/moko-resources/fil/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/fil/strings.xml @@ -651,4 +651,6 @@ Extension ng file Huling Kabanata Base url + Isama ang hash ng URL ng kabanata + Idagdag ang unang anim na karakter ng MD5 hash ng URL ng kabanata sa pangalan ng file o folder ng kabanata. diff --git a/i18n-sy/src/commonMain/moko-resources/fr/strings.xml b/i18n-sy/src/commonMain/moko-resources/fr/strings.xml index 4f56545de..3106ba86b 100644 --- a/i18n-sy/src/commonMain/moko-resources/fr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/fr/strings.xml @@ -143,8 +143,8 @@ Paramètres de tri Utilisez les dernières préférences et sources de pré-migration enregistrées pour migrer en masse Mises à jour des catégories dynamiques de la bibliothèque - Lancez toujours les mises à jour mondiales - Lancer des mises à jour globales uniquement pour les mises à jour de catégories non groupées pour les autres + Toujours lancer des mises à jour globales + Lancer des mises à jour globales uniquement pour les entrées non regroupées, lancer des mises à jour par catégorie pour les autres Lancer des mises à jour de catégorie tout le temps Remplacer le dernier bouton @@ -669,4 +669,10 @@ Synchronise toutes les séries non suivies par MdList vers MangaDex en tant que lecture en cours. Titres similaires Groupes de scantrad à afficher + Inclure le hash de l\'URL du chapitre + Ajoutez les six premiers caractères du hachage MD5 de l\'URL du chapitre au nom du fichier ou du dossier du chapitre. + Nom du fichier + Extension du fichier + URL de base + Chapitre final diff --git a/i18n-sy/src/commonMain/moko-resources/hr/strings.xml b/i18n-sy/src/commonMain/moko-resources/hr/strings.xml index bd3a9ba1a..8880c9e63 100644 --- a/i18n-sy/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/hr/strings.xml @@ -587,7 +587,7 @@ Informacije unosa: Stanje praćenja %1$s prevedeno - ID madija + ID medija Zadnja provjera aktualiziranja Praćene oznake Otvara web prikaz za tvoju E/ExHentai stranicu praćenih oznaka @@ -620,4 +620,10 @@ Nemaš nijedan izvor u svom feedu, idi gore desno za dodavanje izvora Dodati %1$s u feed? Preuzmi teme + Ime datoteke + Sufiks datoteke + Osnovni URL + Završno poglavlje + Uključi hash URL-a poglavlja + Dodaj prvih šest znakova MD5 hash-a URL-a poglavlja imenu datoteke ili mapi poglavlja. diff --git a/i18n-sy/src/commonMain/moko-resources/it/strings.xml b/i18n-sy/src/commonMain/moko-resources/it/strings.xml index 0f2b7ff7f..86590b4dd 100644 --- a/i18n-sy/src/commonMain/moko-resources/it/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/it/strings.xml @@ -336,4 +336,8 @@ Impossibile accedere a Google Drive: %s Non hai effettuato l\'accesso a Google Drive Errore durante l\'eliminazione del file di blocco di Google Drive + Inserisci l\'orario di inizio + Elimina intervallo di tempo + È necessario riavviare l\'applicazione affinché abbia effetto + Proteggi i download con password diff --git a/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml b/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml new file mode 100644 index 000000000..a6d022ff6 --- /dev/null +++ b/i18n-sy/src/commonMain/moko-resources/ko/plurals.xml @@ -0,0 +1,54 @@ + + + + 클린업 완료. %d 폴더를 삭제했습니다 + + + %d 락 타임 + + + 실패한 페이지 %1$d 재시도 중… + + + 정렬 리스트에 %1$d개 태그 추가. 라이브러리에 우선순위 기반 태그 목록을 기준으로 정렬하는 옵션을 추가합니다. 즉, 항목이 원하는 태그를 가진 항목을 우선하는 방식으로 정렬됩니다 + + + %1$d%2$s 항목을 이동하시겠습니까? + + + %1$d%2$s 항목을 복사하시겠습니까? + + + %d개 항목 이동됨 + + + %1$d 페이지 + + + %2$s, %1$d 페이지 + + + %1$d년 전 + + + %1$d개월 전 + + + %1$d주 전 + + + %1$d일 전 + + + %1$d시간 전 + + + %1$d분 전 + + + %1$d초 전 + + + %d 행 + + diff --git a/i18n-sy/src/commonMain/moko-resources/ko/strings.xml b/i18n-sy/src/commonMain/moko-resources/ko/strings.xml new file mode 100644 index 000000000..e9c2fcc93 --- /dev/null +++ b/i18n-sy/src/commonMain/moko-resources/ko/strings.xml @@ -0,0 +1,146 @@ + + + 이동하지 않음 + 수동 검색 + 지금 이동 + 지금 복사 + 타이틀 삭제 + 읽기 시작 + 정보 편집 + 만화(일본) + 만화(한국) + 만화(중국) + 코믹 + 웹툰 + 모든 소스 + E-Hentai + 포크 설정 + MangaDex + E/ExHentai 로그인, 갤러리 동기화 + MangaDex 로그인, 팔로우 동기화 + 버전 %1$s + E-Hentai 웹사이트 계정 설정 + ExHentai 활성화 + 로그인 필요 + Hentai@Home 네트워크 사용 + 가능한 경우 Hentai@Home 네트워크를 통해 이미지를 불러오시겠습니까? 비활성화할 경우 볼 수 있는 페이지 수가 감소합니다\n옵션: \n- 모든 클라이언트 (추천)\n- 기본 포트 클라이언트만 (느려질 수 있음. 비표준 발송 포트를 차단하는 방화벽/프록시 뒤에 있는 경우 활성화) + 모든 클라이언트 (추천) + 기본 포트 클라이언트만 + 검색 결과를 일본어 제목으로 표시 + 현재 검색 결과에 일본어 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정) + 현재 검색 결과에 영어/로마자 제목이 표시됩니다. 설정 변경 후 챕터 캐시를 지우십시오 (고급 설정) + 원본 이미지 사용 + 현재 원본 이미지를 사용하고 있습니다 + 현재 리샘플된 이미지를 사용하고 있습니다 + 관심 태그 + 웹뷰를 열어 E/ExHentai 관심 태그 페이지로 이동합니다 + ExHentai 관심 태그 + 태그 필터 임계값 + -9999 ~ 0 사이의 값이어야 합니다! + 읽은 챕터가 있는 항목을 유지 + 아티스트 + 저자 + 태그에 음수 가중치를 부여해 '내 태그'의 E/ExHentai 페이지에 추가하면 태그를 소프트 필터할 수 있습니다. 갤러리의 태그 가중치가 이 값보다 낮을 경우 표시되지 않습니다. 임계값은 -9999에서 0까지 설정할 수 있습니다. 현재: %1$d + 태그 열람 임계값 + 0 ~ 9999 사이의 값이어야 합니다! + 최근 업로드된 갤러리는 양수 가중치의 최소 한 개의 관심 태그가 있으며, 갤러리의 관심 태그 가중치 합계가 이 값보다 높을 경우 관심 화면에 포함됩니다. 이 임계값은 0에서 9999 사이로 설정할 수 있습니다. 현재: %1$d + 언어 필터 + 갤러리 및 검색 목록에서 특정 언어를 숨기고 싶은 경우, 대화상자에서 해당 갤러리를 선택하십시오.\n해당 갤러리는 검색 결과와 무관하게 표시되지 않습니다.\n요약: 체크 표시 = 제외됨 + 첫 페이지 카테고리 + 첫 페이지와 검색 결과에서 기본적으로 어느 카테고리를 표시하시겠습니까? 해당 필터를 활성화하면 사용할 수 있습니다 + 관심 목록 필터 기본 상태 + ExHentai/E-Hentai를 탐색할 때 관심 목록 필터를 기본적으로 활성화합니다 + 다운로드 이미지 품질 + 이미지 품질 + 자동 + 2400x + 1600x + 1280x + 980x + 780x + 고급 E/ExHentai 탐색 + E/ExHentai 용도로 제작된 고급 탐색 메뉴 활성화/비활성화 + E-Hentai 즐겨찾기 동기화 + 즐겨찾기 업로드 비활성화 + 즐겨찾기는 ExHentai에서만 다운로드됩니다. 앱 내 즐겨찾기 변경사항은 업로드되지 않습니다. 이는 ExHentai에서 즐겨찾기가 실수로 제거되는 것을 방지합니다. 다만 제거된 항목은 여전히 다운로드됩니다(ExHentai에서 즐겨찾기를 제거하면 앱에서도 제거됩니다). + 즐겨찾기 동기화 노트 표시 + 즐겨찾기 동기화 기능에 대한 정보를 표시합니다 + 로그인해 주십시오! + 가능한 경우 동기화 오류를 무시 + 동기화 중 오류가 발생해도 즉시 중단하지 않습니다. 오류 내용은 동기화가 완료되었을 때 표시됩니다. 경우에 따라 즐겨찾기 정보가 손상될 수 있습니다. 대용량 라이브러리 동기화에 유용합니다. + 동기화 상태 강제 리셋 + 다음 동기화 때 전체 재동기화를 수행합니다. 삭제는 동기화되지 않습니다. 앱의 모든 즐겨찾기를 Exhentai에 다시 업로드하며 Exhentai의 모든 즐겨찾기를 앱에 다시 다운로드합니다. 동기화가 중단된 후 복구할 때에 유용합니다. + 동기화 상태 초기화 + 갤러리 업데이트 확인 + 자동 업데이트 제한 + 업데이트 배치 시간 간격 + 갤러리를 업데이트하지 않음 + 1시간 + 2시간 + 3시간 + 6시간 + 12시간 + 24시간 + 48시간 + %1$s(은)는 현재 라이브러리에서 갤러리 업데이트를 확인하지 않습니다. + 업데이트 통계 표시 + 통계 집계 중… + 갤러리 업데이트 통계 + 업데이트가 아직 실행되지 않음. + \n마지막으로 확인된 갤러리:\n- 시간: %1$d\n- 6시간: %2$d\n- 12시간: %3$d\n- 일: %4$d\n- 2일: %5$d\n- 주: %6$d\n- 월: %7$d\n- 연: %8$d + 프로필 노트 설정 + 앱 성능 최적화를 위해 E-Hentai 및 ExHentai에 새 설정 프로필이 추가됩니다. 두 사이트의 프로필이 3개 이하인지 확인해 주십시오.\n\n설정 프로필이 무엇인지 모르는 경우, 중요하지 않을 것이므로 「OK」를 눌러 주십시오. + 설정이 업로드되었습니다! + 설정에 실패했습니다! + 설정 처리 중 오류 발생: %1$s + 설정을 서버로 업로드 중 + 잠시 기다려 주십시오, 시간이 걸립니다… + %1$s 프로필 슬롯이 부족합니다. 프로필을 삭제해 주십시오! + 로그인 상태 재확인 + 대체 로그인 페이지 + 페이지 스타일 변경 넘어가기 + 커스텀 igneous 쿠키 + 일부 사용자는 ExHentai에 정상적인 방법으로 액세스할 수 없으며, 특정 igneous 쿠키 값을 전달해야 합니다. 이 옵션은 해당 사용자를 위한 것입니다. + 개발자 도구 + 통합 hentai 기능 활성화 + 비활성화하면 모든 hentai 기능을 비활성화하는 실험적 기능 + 위임된 소스 활성화 + 다음 소스가 설치된 경우 %1$s 향상 적용: %2$s + 로그 레벨 + 앱 성능에 영향을 미치는 설정입니다. 변경 후 앱을 강제로 재시작합니다. 현재 값: %s + 소스 블랙리스트 활성화 + %1$s 호환되지 않는 확장자/소스를 숨깁니다. 변경 후 앱을 강제로 재시작합니다. + 디버그 메뉴 열기 + 라이브러리가 망가질 수 있습니다!]]> + 정리 시작 중 + 다운로드한 챕터 정리 + 존재하지 않는 폴더, 부분적으로 다운로드된 폴더, 읽은 챕터 폴더를 삭제합니다 + 정리할 폴더 없음 + 고립된 폴더 삭제 + 읽은 폴더 삭제 + 라이브러리에 없는 항목 삭제 + 데이터 세이버 + 리더에서 다운로드 또는 불러오기 전에 이미지를 압축 + 다운로더에 데이터 세이버 사용 + Jpeg 이미지 제외 + Gif 애니메이션 제외 + 이미지 화질 + 값이 높을수록 이미지 화질이 더 많이 보존되지만 파일 크기가 커집니다. 80 퍼센트는 파일 크기와 이미지 화질 사이의 좋은 타협점입니다 + Jpeg로 압축 + Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Jpeg로 압축 + Jpeg 파일 크기는 Webp(데이터 보존 중심)보다 작지만, 화질이 더 낮아집니다.\n현재는 Webp로 압축 + 흑백으로 변환 + Bandwidth Hero (Bandwidth Hero 프록시 서버 필요) + wsrv.nl + Bandwidth Hero 프록시 서버 + 여기에 Bandwidth Hero 프록시 서버 url 입력 + 챕터 URL 해시 포함 + 챕터 URL의 MD5 해시의 처음 여섯 글자를 챕터 파일 또는 폴더 이름에 추가합니다. + 최소 + 확장 + 최대 + 치명적 오류만 + 전체 로그 + 네트워크 검사 모드 + 모든 검색 필터를 기본으로 확장 + diff --git a/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml b/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml index 548d58114..5bb31aa2f 100644 --- a/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/pt-rBR/strings.xml @@ -531,7 +531,7 @@ Usar economia de dados no baixador Bandwidth Hero (Requer um servidor proxy Bandwidth Hero) wsrv.nl - Mesclar em transborde + Mesclar em caso de excesso Colocar o botão de mesclar no menu flutuante ao invés da pagina inicial Você quer que a aba Feed seja a primeira aba em Navegar? Isso irá fazer dela a aba principal ao abrir Navegar, não é recomendado se você estiver usando uma rede medida ou dados móveis Gatilhos @@ -542,7 +542,7 @@ Frequência de sincronização Error ao Deletar Ficheiro de Bloqueio no Google Drive Pode ser usado para criar gatilhos de sincronização - Sincronizar após Leitura de Capitulo + Sincronizar com a leitura do capítulo Confirmação de Limpeza Criar gatilhos de sincronização Sincronizar após abrir capítulo @@ -655,4 +655,11 @@ Prólogo Eliminar duplicatas com base na prioridade Sem remoção de duplicatas + Incluir o hash da URL do capítulo + Adicione os seis primeiros caracteres do hash MD5 da URL do capítulo ao nome do arquivo ou pasta do capítulo. + Selecione entradas usando metadados de origem + Seleciona automaticamente o título correspondente se a fonte fornecer links para rastreadores. Atualmente compatível com MangaDex + HABILITAR ESTA OPÇÃO CRIARÁ UM NOVO BANCO DE DADOS. SIGA ESTAS ETAPAS PARA MANTER SEUS DADOS SEGUROS
1. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> CRIAR BACKUP
2. CONFIGURAÇÕES DO SISTEMA -> LIMPAR DADOS DO APP
3. ABRA O APP E ATIVE ISSO
4. CONFIGURAÇÕES DO SISTEMA -> FORÇAR REINICIALIZAÇÃO
5. CONFIGURAÇÕES -> DADOS E ARMAZENAMENTO -> RESTAURAR BACKUP]]>
+ AES 256 + AES 128 diff --git a/i18n-sy/src/commonMain/moko-resources/ru/strings.xml b/i18n-sy/src/commonMain/moko-resources/ru/strings.xml index 4b0858c1f..cd728dd08 100644 --- a/i18n-sy/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/ru/strings.xml @@ -694,4 +694,6 @@ Расширение файла Главный URL Последняя глава + Включать URL хэш главы + Добавить первые шесть символов хеша MD5 URL-адреса главы к имени файла или папки главы. diff --git a/i18n-sy/src/commonMain/moko-resources/tr/strings.xml b/i18n-sy/src/commonMain/moko-resources/tr/strings.xml index 805b77432..14eabb326 100644 --- a/i18n-sy/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/tr/strings.xml @@ -567,7 +567,7 @@ Takip etme durumu Mal id Görülebilir: %1$s - %1$s ÇVR + %1$s Çeviri Emin misiniz? Girdi birleştirmeden kaldırılacaktır, ayrıca birleştirilmiş girdiye uygulanmış kaydedilmeyen değişiklikler kaybolacaktır Bölüm güncellemelerini ayarla @@ -624,4 +624,6 @@ Dosya uzantısı Ana url Son bölüm + Bölüm URL hash\'ini ekle + Bölüm URL\'sinin MD5 hash değerinin ilk altı karakterini bölüm dosyasının veya klasörünün adına ekleyin. diff --git a/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml index 4e29c759e..6ccea123d 100644 --- a/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -682,4 +682,6 @@ 文件拓展名 终章 基础url + 包含章节URL哈希值 + 将章节URL的MD5哈希值的前六个字符添加到章节文件或文件夹名中。 diff --git a/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml index 4e2f5d0e2..ccdc2dc34 100644 --- a/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n-sy/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -682,4 +682,6 @@ 副檔名 基礎url 終章 + 包含章節網址雜湊值 + 將章節網址的 MD5 雜湊值前六個字元附加至章節檔案或資料夾名。 diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 608a39356..e80e42b65 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -330,7 +330,7 @@ Updates pending Update Update all - Obsolete + Orphaned Install Pending Downloading @@ -344,7 +344,7 @@ App info Untrusted extension Malicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension, you accept these risks. - This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended. + This extension is not associated with any store. It will not receive updates, may not function properly, and can cause issues with the app. Uninstalling it is recommended. Do you really want to remove \"%s\" extension? Failed to fetch available extensions Version @@ -364,20 +364,19 @@ Revoke trusted unknown extensions - Extension repos - You have no repos set. - Add repo - Repo URL - Add additional repos to %s. This should be a URL that ends with \"index.min.json\". - This repo already exists! - Delete repo - Invalid repo URL - Do you wish to delete the repo \"%s\"? - Do you wish to add the repo \"%s\"? Open source repo - Replace - Signing Key Fingerprint Already Exists - Repository %1$s has the same Signing Key Fingerprint as %2$s.\nIf this is expected, %2$s will be replaced, otherwise contact your repo maintainer. + + + Extension stores + Add extension store + You haven\'t added an extension store yet + Add extension store + This extension store already exists + Processing… + Store index URL + Do you wish to add the extension store below? + Remove extension store + Do you wish to remove the extension store \"%1$s\" (%2$s)? Fullscreen @@ -567,7 +566,6 @@ What do you want to backup? App settings Source settings - Extension repos Include sensitive settings (e.g., tracker login tokens) All read entries Creating backup @@ -759,6 +757,7 @@ By upload date Download Unread + Bookmarked Custom cover Cover Cover saved diff --git a/i18n/src/commonMain/moko-resources/cs/strings.xml b/i18n/src/commonMain/moko-resources/cs/strings.xml index 5067b588b..2131871b0 100644 --- a/i18n/src/commonMain/moko-resources/cs/strings.xml +++ b/i18n/src/commonMain/moko-resources/cs/strings.xml @@ -710,7 +710,7 @@ Synchronizace knihovny dokončena Přihlášení sledovače Klepněte zde pro pomoc s Cloudflare - Index stažených zneplatněn + Obnovování indexu stažených Nelze vytvořit soubor zálohy Bez připojení k internetu Nelze dosáhnout %s @@ -869,4 +869,49 @@ Zajišťuje kompatibilitu s určitými úložnými médii, která nepodporují Unicode. Pokud je tato funkce povolena, budete muset ručně přejmenovat názvy složkek source a manga tak, že nahradíte non-ASCII znaky jejich malými hexadecimálními znaky v UTF-8. Názvy souborů kapitol není nutné přejmenovávat. Souběžné stahování zdrojů Souběžné stahování stránek + Zavřít okno + Vypočítávání… + Souběžně stahované stránky na zdroj + Vybrat vše + Vybrat žádné + Vybrat povolené zdroje + Vybrat připnuté zdroje + Pokračovat + Data pro migraci + Smazat stažené položky po migraci + Migrace + Migrace (%1$d/%2$d) + Kopírovat + Migrovat + Nebyly nalezeny žádné alternativy + Poslední: %1$s + Neznámý + Vyhledat manuálně + Nemigrovat + Migrovat nyní + Kopírovat nyní + Zastavit migraci? + Zastavit + Zrušit + Kopírovat + Migrovat + Zrušit + Zrušit + Žádné kapitoly nebyly nalezeny, tuto položku nelze použít pro migraci + Založeno + Filtrovat vyloučené scanlatory + Vybrané + Dostupné + Dodatečná klíčová slova (volitelné) + Pomáhá zúžit výsledky vyhledávání přidáním dalších klíčových slov + Skrýt položky bez shody + Skrýt položky bez nových kapitol + Zobrazit položku, pouze pokud má shoda další kapitoly + Tyto možnosti jsou pomalé, nebezpečné a mohou vést k omezení ze strany zdrojů + Pokročilý režim vyhledávání + Rozdělí název na klíčová slova pro rozsáhlejší vyhledávání + Shoda podle čísla kapitoly + Je-li povoleno, vybere shodu, která je nejdále. V opačném případě vybere první shodu podle priority zdroje. + Zobrazovat obrázky v popisu mangy + Přečtené kapitoly a postup u položek mimo knihovnu budou ztraceny diff --git a/i18n/src/commonMain/moko-resources/de/strings.xml b/i18n/src/commonMain/moko-resources/de/strings.xml index 3e889c1ff..36b77d18b 100644 --- a/i18n/src/commonMain/moko-resources/de/strings.xml +++ b/i18n/src/commonMain/moko-resources/de/strings.xml @@ -170,8 +170,8 @@ Pausiert Quellentitel Kapitelnummer - Quelle - Kapitelnummer + Nach Quelle + Nach Kapitelnummer Herunterladen Ungelesen Bist du sicher, dass du die ausgewählten Kapitel löschen willst? @@ -911,4 +911,5 @@ Tab schließen Ausgeschlossene Scanlationgruppen herausfiltern Wird berechnet… + Mit Lesezeichen versehen diff --git a/i18n/src/commonMain/moko-resources/el/strings.xml b/i18n/src/commonMain/moko-resources/el/strings.xml index 28392e9dc..9ad30cf58 100644 --- a/i18n/src/commonMain/moko-resources/el/strings.xml +++ b/i18n/src/commonMain/moko-resources/el/strings.xml @@ -709,7 +709,7 @@ Συγχρονισμός βιβλιοθήκης Ο συγχρονισμός βιβλιοθήκης ολοκληρώθηκε Πατήστε εδώ για βοήθεια με το Cloudflare - Το ευρετήριο λήψεων ακυρώθηκε + Αναδημιουργία ευρετηρίου λήψεων Σύνδεση παρακολούθησης Δεν ήταν δυνατή η δημιουργία αντιγράφου ασφαλείας Δεν ήταν δυνατή η πρόσβαση στο %s @@ -911,4 +911,7 @@ Ταυτόχρονες λήψεις πηγών Ταυτόχρονες λήψεις σελίδων Σελίδες που λήφθηκαν ταυτόχρονα ανά πηγή + Υπολογισμός… + Προστέθηκε στους σελιδοδείκτες + Φιλτράρισμα εξαιρούμενων scanlator diff --git a/i18n/src/commonMain/moko-resources/eo/strings.xml b/i18n/src/commonMain/moko-resources/eo/strings.xml index d47265896..0fe38264c 100644 --- a/i18n/src/commonMain/moko-resources/eo/strings.xml +++ b/i18n/src/commonMain/moko-resources/eo/strings.xml @@ -697,7 +697,7 @@ Devigas restartigi apon por efektiviĝi Restarigas legadan reĝimon kaj orientiĝon de ĉiuj serioj Reindeksi elŝutojn - Indeksoj de elŝutoj nevalidigitaj + Rekreado de indekso de elŝutoj Forigi historion de elementoj ne konservitaj en biblioteko %1$d nebibliotekaj elementoj en datumbazo Nenio por viŝi @@ -901,4 +901,6 @@ Kunrulaj elŝutoj de paĝoj Paĝoj elŝutitaj sammomente por fonto Filtri ekskluzivitajn skantradukistojn + Kalkulado… + Legosignita diff --git a/i18n/src/commonMain/moko-resources/es/strings.xml b/i18n/src/commonMain/moko-resources/es/strings.xml index 3978bdc83..414fdfac6 100644 --- a/i18n/src/commonMain/moko-resources/es/strings.xml +++ b/i18n/src/commonMain/moko-resources/es/strings.xml @@ -906,4 +906,5 @@ Cerrar pestaña Ocultar equipos de traducción excluidos Calculando… + Favoritos diff --git a/i18n/src/commonMain/moko-resources/hr/strings.xml b/i18n/src/commonMain/moko-resources/hr/strings.xml index 192077ea1..af8a499fe 100644 --- a/i18n/src/commonMain/moko-resources/hr/strings.xml +++ b/i18n/src/commonMain/moko-resources/hr/strings.xml @@ -913,4 +913,5 @@ Zatvori karticu Izračunavanje … Filtriraj isključene amaterske prevoditelje + Zabilježeno diff --git a/i18n/src/commonMain/moko-resources/ru/strings.xml b/i18n/src/commonMain/moko-resources/ru/strings.xml index e5ea6f666..3e5e7427d 100644 --- a/i18n/src/commonMain/moko-resources/ru/strings.xml +++ b/i18n/src/commonMain/moko-resources/ru/strings.xml @@ -911,4 +911,5 @@ Число одновременных загрузок страниц с источника Фильтровать исключенных переводчиков Вычисление… + В закладках diff --git a/i18n/src/commonMain/moko-resources/sk/strings.xml b/i18n/src/commonMain/moko-resources/sk/strings.xml index 36c866d7e..15b3239b5 100644 --- a/i18n/src/commonMain/moko-resources/sk/strings.xml +++ b/i18n/src/commonMain/moko-resources/sk/strings.xml @@ -166,7 +166,7 @@ Štvrtá predposledná prečítaná kapitola Piata posledná prečítaná kapitola Stiahnuť nové kapitoly - Služby + Sledovače Vytvoriť zálohu Možno použiť na obnovenie aktuálnej knižnice Obnoviť zálohu @@ -406,8 +406,8 @@ Odstrániť kapitoly Uložiť ako archív CBZ Zlepšuje výkon čítačky - Návod k sledovaniu - Rozšírené služby + Príručka k sledovačom + Vylepšené sledovače Sieť Vymazanie vyrovnávacej pamäte kapitoly pri zatvorení aplikácie Čo je nové @@ -450,7 +450,7 @@ Zálohovanie/obnovenie nemusí fungovať správne, ak je vypnutá optimalizácia MIUI. Zakázať optimalizáciu batérie Teraz ste odhlásení - Návod pre lokálne zdroje + Príručka pre lokálne zdroje Neznámy autor Nepodarilo sa skopírovať do schránky Nepodarilo sa nájsť cestu k súboru stránky %d @@ -491,7 +491,7 @@ Vymazať históriu Chyba pri ukladaní obalu Nastaviť ako predvolené - Jednosmerná synchronizácia na aktualizáciu priebehu kapitol v službách sledovania. Nastavte sledovanie pre jednotlivé položky mangy z ich tlačidla sledovania. + Jednosmerná synchronizácia postupu v mange na externé sledovacie služby (sledovače). Nastavte sledovanie pre jednotlivé položky mangy z ich tlačidla sledovania. Režim čítania Aktualizácie kapitol V databáze je manga %1$d, ktorá sa nenachádza v knižnici @@ -551,7 +551,7 @@ Čakajúce aktualizácie Ďalšia strana Otvoriť na GitHub-e - Príručka Začíname + Príručka pre začiatočníkov Zakázať režim inkognito Menej Služby, ktoré poskytujú rozšírené funkcie pre konkrétne zdroje. Mangy sú automaticky sledované po pridaní do vašej knižnice. @@ -577,7 +577,7 @@ Nie je možné otvoriť poslednú prečítanú kapitolu Klepnutím sa dozviete viac Stiahnuté kapitoly - Návod k migrácií zdrojov + Príručka k migrácií zdrojov Všetky nastavenia čítačky sa obnovia Aktivita na pozadí Pomáha s aktualizáciami a zálohovaním knižnice na pozadí @@ -640,10 +640,10 @@ Aktualizovať kategóriu Kopírovať do schránky OK - Presunúť sériu na koniec - Ďalšie + Presunúť sériu na spodok + Ďalej Preskočiť - Povolenie pre sťahovanie aplikácie + Povolenie na inštalovanie ľubovoľných aplikácii Bezpečnosť Začíname Odstrániť rozšírenie? @@ -665,4 +665,97 @@ Nadchádzajúce Nahradiť Navigovať hore + Prispieť finančne + Auto + Vlastná frekvencia aktualizácie + Skóre na sledovači + Neprečítané kapitoly + Pokračovať v čítaní tlačidlo + Poznámky + Upraviť poznámky + Vrátiť na predvolené + Zatvoriť kartu + Nie teraz + Ajtak pridať + Vypočítavam… + Príručka na úvod + Poďme najprv nastaviť nejaké veci. Vždy ich môžete neskôr zmeniť. + Vyberte priečinok, kde %1$s bude ukladať stiahnuté kapitoly, zálohy a ďalšie.\n\nOdporúčame použiť samostatný priečinok špecificky pre túto aplikáciu.\n\nVybraný priečinok: %2$s + Aktualizujete zo staršej verzie, a nie ste si istí, čo vybrať? Viac informácií nájdete v príručke úložiska. + Príručka úložiska + Na inštaláciu rozšírení zdrojov. + Povolenie posielania notifikácii + Notifikácie o aktualizáciach v knižnici a podobne. + Využitie batérie v pozadí + Vyhnite sa prerušeniam aktualizácie knižnice, sťahovania a obnovy záloh. + Poslať záznamy v prípade zlyhania aplikácie + Poslať anonymizované záznamy v prípade zlyhania aplikácie vývojárom. + Povoliť analytické dáta + Posielať anonymizované dáta o používaní aplikácie vývojárom pre zlepšenie zážitku. + Povoliť + Prvý krát používate %s? Odporúčame prečítať si príručku pre začiatočníkov. + Inštalujete %s znovu? + Téma, dátum a formát času + Príručka pre nadchádzajúce + Prihlásenie do sledovača + Zahrnúť citlivé nastavenia (napr. prihlasovacie údaje do sledovačov) + Aktualizovať postup na sledovačoch po kapitolu %d? + Sledovače aktualizované na kapitolu %d + Sledovače + Kategórie, globálna aktualizácia, prechádzanie kapitolami + Režim čítania, displej, navigácia + Automatické sťahovanie, sťahovanie dopredu + Jednosmerná synchronizácia postupu, vylepšená synchronizácia + Zdroje, rozšírenia, globálne vyhľadávanie + Manuálne & automatické zálohy, miesto v úložisku + Zamknutie aplikácie, bezpečná obrazovka + Uložiť záznamy o zlyhaní aplikácie, optimalizácie batérie + Monochromatická + Relatívne časové značky + \"%1$s\" namiesto \"%2$s\" + Zobraziť obrázky v popisoch mangy + Analytické dáta a záznamy v prípade zlyhania + Posielaním záznamov v prípade zlyhania aplikácie a analytických údajov nám umožníte identifikovať a opraviť chyby, zlepšiť rýchlost aplikácie a urobiť aplikáciu viac relevantnejšiu pre vaše potreby + Inteligentné aktualizácie + Zobraziť počet neprečítaných na ikone Aktualizácii + Správanie + Kapitola po potiahnutí prstom doprava + Kapitola po potiahnutí prstom doľava + Označiť duplikátne kapitoly ako prečítané + Po prečítaní kapitoly + Po získaní novej kapitoly + Schovať chýbajúce indikátory kapitol + Zakázať názvy súborov, ktoré nie sú v kódovaní ASCII + Zaručí kompatibilitu s niektorými úložiskami, ktoré nepodporujú kódovanie Unicode. Keď je táto možnosť zvolená, budete musieť manuálne premenovať priečinky zdrojov a mangy tak, že nahradíte znaky, ktoré nie sú podporované v kódovaní ASCII, ich malými hexadecimálnymi reprezentáciami. Súbory kapitol netreba premenovávať. + Odstrániť + Na inštaláciu rozšírení sú potrebné povolenia. Ťuknite sem, aby ste ich udelili. + Odstrániť status \"dôveryhodné\" z neznámych rozšírení + Repozitáre rozšírení + Nemáte nastavené žiadne repozitáre. + Pridať repozitár + URL Repozitára + Pridať ďalšie repozitáre do %s. Hodnota by mala byť URL, ktorá končí \"index.min.json\". + Tento repozitár už existuje! + Odstrániť repozitár + Neplatná URL repozitára + Naozaj chcete odstrániť repozitár \"%s\"? + Chcete pridať repozitár \"%s\"? + Open-source repozitár + Otlačok podpisového kľúča už existuje + Repozitár %1$s má rovnaký otlačok podpisového kľúča ako %2$s.\nAk je toto očakávané, %2$s bude nahradený, inak kontaktuje manažéra repozitára. + Otočiť široké strany, aby sa zmestili + Preklopiť orientáciu otočených širokých strán + Dvojťuk pre priblíženie + Prebliknúť pri zmene strany + Znižuje ghosting na e-ink displejoch + Trvanie prebliknutia + Prebliknúť každých + Prebliknúť s + Čiernou + Bielou + Čiernou aj Bielou + Vlastný prah pre hardvérové bitmapy + Predvolený (%d) + Ak čítacie rozhranie načítava prázdne obrázky, postupne znížte prah.\nVybrané: %s + Použiť starší dekodér pre čítacie rozhranie \"dlhé pásy\" diff --git a/i18n/src/commonMain/moko-resources/tr/strings.xml b/i18n/src/commonMain/moko-resources/tr/strings.xml index 4ee172b46..4126b5da1 100644 --- a/i18n/src/commonMain/moko-resources/tr/strings.xml +++ b/i18n/src/commonMain/moko-resources/tr/strings.xml @@ -913,4 +913,5 @@ Sekmeyi kapat Hariç tutulan Çevirenleri filtrele Hesaplanıyor… + Yer imli diff --git a/i18n/src/commonMain/moko-resources/vi/strings.xml b/i18n/src/commonMain/moko-resources/vi/strings.xml index abe1ee8ff..c37cad67c 100644 --- a/i18n/src/commonMain/moko-resources/vi/strings.xml +++ b/i18n/src/commonMain/moko-resources/vi/strings.xml @@ -912,4 +912,6 @@ Các trang được tải xuống đồng thời theo từng nguồn Đóng thẻ Lọc nhóm dịch bị loại trừ + Đang tính… + Đã đánh dấu diff --git a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml index a662d6c33..8fd311a76 100644 --- a/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rCN/strings.xml @@ -911,4 +911,5 @@ 关闭标签 筛选排除翻译组 计算中… + 已添加书签 diff --git a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml index 2217081b1..79d81a9b6 100644 --- a/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml +++ b/i18n/src/commonMain/moko-resources/zh-rTW/strings.xml @@ -903,4 +903,6 @@ 每個來源同時下載的頁數 關閉頁籤 篩選排除掃譯者 + 計算中… + 已新增書籤 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 399ad9fad..762efb2c7 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 @@ -158,6 +158,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga(page)")) open fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) @@ -203,6 +204,7 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga(page, query, filters)")) open fun fetchSearchManga( page: Int, @@ -261,6 +263,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page number to retrieve. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates(page)")) open fun fetchLatestUpdates(page: Int): Observable { return client.newCall(latestUpdatesRequest(page)) @@ -305,6 +308,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to be updated. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails(manga)")) open fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) @@ -400,6 +404,7 @@ abstract class HttpSource : CatalogueSource { * * @param manga the manga to look for chapters. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList(manga)")) open fun fetchChapterList(manga: SManga): Observable> { return client.newCall(chapterListRequest(manga)) @@ -453,6 +458,7 @@ abstract class HttpSource : CatalogueSource { * * @param chapter the chapter whose page list has to be fetched. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList(chapter)")) open fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) @@ -499,6 +505,7 @@ abstract class HttpSource : CatalogueSource { * * @param page the page whose source image has to be fetched. */ + @Suppress("DEPRECATION") @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl(page)")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) diff --git a/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt b/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt deleted file mode 100644 index 37fcb3886..000000000 --- a/source-api/src/commonMain/kotlin/exh/metadata/metadata/HBrowseSearchMetadata.kt +++ /dev/null @@ -1,76 +0,0 @@ -package exh.metadata.metadata - -import android.content.Context -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.model.copy -import kotlinx.serialization.Serializable -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR - -@Serializable -class HBrowseSearchMetadata : RaisedSearchMetadata() { - var hbId: Long? = null - - var hbUrl: String? = null - - var thumbnail: String? = null - - var title: String? by titleDelegate(TITLE_TYPE_MAIN) - - // Length in pages - var length: Int? = null - - override fun createMangaInfo(manga: SManga): SManga { - val key = hbUrl - - val title = title - - // Guess thumbnail URL if manga does not have thumbnail URL - val cover = if (manga.thumbnail_url.isNullOrBlank()) { - guessThumbnailUrl(hbId.toString()) - } else { - null - } - - val artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name } - - val genres = tagsToGenreString() - - val description = null - - return manga.copy( - url = key ?: manga.url, - title = title ?: manga.title, - thumbnail_url = cover ?: manga.thumbnail_url, - artist = artist, - genre = genres, - description = description, - ) - } - - override fun getExtraInfoPairs(context: Context): List> { - return with(context) { - listOfNotNull( - getItem(hbId) { stringResource(SYMR.strings.id) }, - getItem(hbUrl) { stringResource(SYMR.strings.url) }, - getItem(thumbnail) { stringResource(SYMR.strings.thumbnail_url) }, - getItem(title) { stringResource(MR.strings.title) }, - getItem(length) { stringResource(SYMR.strings.page_count) }, - ) - } - } - - companion object { - const val BASE_URL = "https://www.hbrowse.com" - - private const val TITLE_TYPE_MAIN = 0 - - const val TAG_TYPE_DEFAULT = 0 - const val ARTIST_NAMESPACE = "artist" - - fun guessThumbnailUrl(hbid: String): String { - return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed" - } - } -} diff --git a/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt b/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt deleted file mode 100644 index dc9a640b0..000000000 --- a/source-api/src/commonMain/kotlin/exh/metadata/metadata/TsuminoSearchMetadata.kt +++ /dev/null @@ -1,114 +0,0 @@ -package exh.metadata.metadata - -import android.content.Context -import androidx.core.net.toUri -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.model.copy -import exh.metadata.MetadataUtil -import exh.util.nullIfEmpty -import kotlinx.serialization.Serializable -import tachiyomi.core.common.i18n.stringResource -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import java.text.SimpleDateFormat -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.util.Locale - -@Serializable -class TsuminoSearchMetadata : RaisedSearchMetadata() { - var tmId: Int? = null - - var title by titleDelegate(TITLE_TYPE_MAIN) - - var artist: String? = null - - var uploadDate: Long? = null - - var length: Int? = null - - var ratingString: String? = null - - var averageRating: Float? = null - - var userRatings: Long? = null - - var favorites: Long? = null - - var category: String? = null - - var collection: String? = null - - var group: String? = null - - var parody: List = emptyList() - - var character: List = emptyList() - - override fun createMangaInfo(manga: SManga): SManga { - val title = title - val cover = tmId?.let { BASE_URL.replace("www", "content") + thumbUrlFromId(it.toString()) } - - val artist = artist - - val status = SManga.UNKNOWN - - // Copy tags -> genres - val genres = tagsToGenreString() - - val description = null - - return manga.copy( - title = title ?: manga.title, - thumbnail_url = cover ?: manga.thumbnail_url, - artist = artist ?: manga.artist, - status = status, - genre = genres, - description = description, - ) - } - - override fun getExtraInfoPairs(context: Context): List> { - return with(context) { - listOfNotNull( - getItem(tmId) { stringResource(SYMR.strings.id) }, - getItem(title) { stringResource(MR.strings.title) }, - getItem(uploader) { stringResource(SYMR.strings.uploader) }, - getItem( - uploadDate, - { - MetadataUtil.EX_DATE_FORMAT - .format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())) - }, - ) { - stringResource(SYMR.strings.date_posted) - }, - getItem(length) { stringResource(SYMR.strings.page_count) }, - getItem(ratingString) { stringResource(SYMR.strings.rating_string) }, - getItem(averageRating) { stringResource(SYMR.strings.average_rating) }, - getItem(userRatings) { stringResource(SYMR.strings.total_ratings) }, - getItem(favorites) { stringResource(SYMR.strings.total_favorites) }, - getItem(category) { stringResource(SYMR.strings.genre) }, - getItem(collection) { stringResource(SYMR.strings.collection) }, - getItem(group) { stringResource(SYMR.strings.group) }, - getItem(parody.nullIfEmpty(), { it.joinToString() }) { stringResource(SYMR.strings.parodies) }, - getItem(character.nullIfEmpty(), { it.joinToString() }) { stringResource(SYMR.strings.characters) }, - ) - } - } - - companion object { - private const val TITLE_TYPE_MAIN = 0 - - const val TAG_TYPE_DEFAULT = 0 - - val BASE_URL = "https://www.tsumino.com" - - val TSUMINO_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US) - - fun tmIdFromUrl(url: String) = url.toUri().lastPathSegment - - fun thumbUrlFromId(id: String) = "/thumbs/$id/1" - } -}