diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 61ca4b433b..1546dee1dd 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -2,13 +2,12 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:base"], "labels": ["Dependencies"], + "semanticCommits": "disabled", "packageRules": [ { - "groupName": "Compose BOM (Alpha)", - "matchPackageNames": [ - "dev.chrisbanes.compose:compose-bom" - ], - "ignoreUnstable": false + "groupName": "GitHub Actions", + "matchManagers": ["github-actions"], + "pinDigests": true, } ] -} \ No newline at end of file +} diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 00d3391362..3d7b2aaff9 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -1,12 +1,18 @@ name: PR build check on: pull_request: - paths-ignore: - - '**.md' - - 'i18n/src/commonMain/moko-resources/**/strings-aniyomi.xml' - - 'i18n/src/commonMain/moko-resources/**/strings.xml' - - 'i18n/src/commonMain/moko-resources/**/plurals-aniyomi.xml' - - 'i18n/src/commonMain/moko-resources/**/plurals.xml' + paths: + - '**' + - '!**.md' + - '!i18n/src/commonMain/moko-resources/**/strings-aniyomi.xml' + - '!i18n/src/commonMain/moko-resources/**/strings.xml' + - '!i18n/src/commonMain/moko-resources/**/plurals-aniyomi.xml' + - '!i18n/src/commonMain/moko-resources/**/plurals.xml' + - 'i18n/src/commonMain/moko-resources/base/strings-aniyomi.xml' + - 'i18n/src/commonMain/moko-resources/base/strings.xml' + - 'i18n/src/commonMain/moko-resources/base/plurals-aniyomi.xml' + - 'i18n/src/commonMain/moko-resources/base/plurals.xml' + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} @@ -22,33 +28,34 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - name: Dependency Review - uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 - name: Set up JDK - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: 17 distribution: adopt + - name: Set up gradle + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + - name: Build app and run unit tests - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - with: - arguments: detekt assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: arm64-v8a-${{ github.sha }} path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk - name: Upload mapping - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: mapping-${{ github.sha }} - path: app/build/outputs/mapping/standardRelease \ No newline at end of file + path: app/build/outputs/mapping/standardRelease diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 0412a954f7..729ab2abcd 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -6,8 +6,7 @@ permissions: on: push: branches: - - master - - dev + - '*' tags: - v* @@ -22,30 +21,31 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - name: Set up JDK - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: 17 distribution: adopt + - name: Set up gradle + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + - name: Build app and run unit tests - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - with: - arguments: detekt assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest + run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: arm64-v8a-${{ github.sha }} path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk - name: Upload mapping - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: mapping-${{ github.sha }} path: app/build/outputs/mapping/standardRelease @@ -98,7 +98,7 @@ jobs: - name: Create Release if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi' - uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: tag_name: ${{ env.VERSION_TAG }} name: Aniyomi ${{ env.VERSION_TAG }} diff --git a/.gitignore b/.gitignore index 0c5f5664ec..42c569b7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,16 @@ +# Build files .gradle .kotlin -/local.properties -/acra.properties -/.idea/workspace.xml -.DS_Store +build + +# IDE files +*.iml .idea/* !.idea/icon.png -*iml -*.iml +/captures -# Built files -*/build -/build -*.apk -app/**/output.json +# Configuration files +local.properties -# Unnecessary file -*.swp \ No newline at end of file +# macOS specific files +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..41f5679d63 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,66 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is a modified version of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +- `Added` - for new features. +- `Changed ` - for changes in existing functionality. +- `Improved` - for enhancement or optimization in existing functionality. +- `Removed` - for now removed features. +- `Fixed` - for any bug fixes. +- `Other` - for technical stuff. + +## [Unreleased] +### Added + +- feat(external-players): add mpvKt ([@Secozzi](https://github.com/Secozzi)) ([#1674](https://github.com/aniyomiorg/aniyomi/pull/1674)) +- feat(player): video filters ([@abdallahmehiz](https://github.com/abdallahmehiz)) ([#1698](https://github.com/aniyomiorg/aniyomi/pull/1698)) +- feat(player): Add better auto sub select ([@Secozzi](https://github.com/Secozzi)) ([#1706](https://github.com/aniyomiorg/aniyomi/pull/1706)) +- feat(downloader): Copy the file location when using ext downloader ([@quickdesh](https://github.com/quickdesh)) ([#1758](https://github.com/aniyomiorg/aniyomi/pull/1758)) + +### Improved + +- feat(entry): show "Now" instead of "0 minutes ago" ([@Secozzi](https://github.com/Secozzi)) ([#1715](https://github.com/aniyomiorg/aniyomi/pull/1715)) + +### Fixed + +- Fix enhanced tracking for jellyfin ([@Secozzi](https://github.com/Secozzi)) ([#1656](https://github.com/aniyomiorg/aniyomi/pull/1656), [#1658](https://github.com/aniyomiorg/aniyomi/pull/1658)) +- fix(animescreen): Fix airing time not showing ([@Secozzi](https://github.com/Secozzi)) ([#1720](https://github.com/aniyomiorg/aniyomi/pull/1720)) +- fix hidden categories getting reset after delete/reorder ([@cuong-tran](https://github.com/cuong-tran)) ([#1780](https://github.com/aniyomiorg/aniyomi/pull/1780)) +- Fix episode progress not being saved and duplicate tracks ([@perokhe](https://github.com/perokhe)) ([#1784](https://github.com/aniyomiorg/aniyomi/pull/1784), [#1785](https://github.com/aniyomiorg/aniyomi/pull/1785)) + +### Other + +- Merge from mihon until 0.16.5 ([@Secozzi](https://github.com/Secozzi)) ([#1663](https://github.com/aniyomiorg/aniyomi/pull/1663)) + - Merge until latest mihon commits ([@Secozzi](https://github.com/Secozzi)) ([#1693](https://github.com/aniyomiorg/aniyomi/pull/1693)) + - Merge until latest mihon commits (v0.17.0) ([@Secozzi](https://github.com/Secozzi)) ([#1804](https://github.com/aniyomiorg/aniyomi/pull/1804)) + +## [v0.16.4.3] - 2024-07-01 +### Fixed + +- Fix extensions disappearing due to errors with the ClassLoader ([@jmir1](https://github.com/jmir1)) ([`959f84a`](https://github.com/aniyomiorg/aniyomi/commit/959f84ab41859f90c458c076d83d363ae086e47f)) + +## [v0.16.4.2] - 2024-07-01 +### Fixed + +- Hotfix to eliminate all proguard issues causing errors and crashes ([@jmir1](https://github.com/jmir1)) ([`a8cd723`](https://github.com/aniyomiorg/aniyomi/commit/a8cd7233dfdf26c98ff86b1871a7ac5774379b5e), [`a7644c2`](https://github.com/aniyomiorg/aniyomi/commit/a7644c268153fc0b9f10c27202591f960c6f6384), [`5045fa1`](https://github.com/aniyomiorg/aniyomi/commit/5045fa18ce5a1faa2130f1a33609e43d8453f078)) + +## [v0.16.4.1] - 2024-07-01 +### Fixed + +- Hotfix release to address errors with extensions ([@jmir1](https://github.com/jmir1)) ([`98d2528`](https://github.com/aniyomiorg/aniyomi/commit/98d252866e17beba7d9a4d094797e23c05ead6c1)) + +## [v0.16.4.0] - 2024-07-01 +### Fixed + +- fix(pip): pip not broadcasting intent in A14+ ([@quickdesh](https://github.com/quickdesh)) ([#1603](https://github.com/aniyomiorg/aniyomi/pull/1603)) +- fix: advanced player settings crash in android ≤ 10 ([@perokhe](https://github.com/perokhe)) ([#1627](https://github.com/aniyomiorg/aniyomi/pull/1627)) + +### Improved + +- feat: hide the skip intro button if the skipped amount == 0 ([@abdallahmehiz](https://github.com/abdallahmehiz)) ([#1598](https://github.com/aniyomiorg/aniyomi/pull/1598)) + +### Other + +- Merge from mihon until mihon 0.16.2 ([@Secozzi](https://github.com/Secozzi)) ([#1578](https://github.com/aniyomiorg/aniyomi/pull/1578)) + - Merge from mihon until 0.16.4 ([@Secozzi](https://github.com/Secozzi)) ([#1601](https://github.com/aniyomiorg/aniyomi/pull/1601)) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cca4def05..6a687ebdca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,10 +24,6 @@ Before you start, please note that the ability to use following technologies is - [Android Studio](https://developer.android.com/studio) - Emulator or phone with developer options enabled to test changes. -## Linting - -To auto-fix some linting errors, run the `ktlintFormat` Gradle task. - ## Getting help - Join [the Discord server](https://discord.gg/F32UjdJZrR) for online help and to ask questions while developing. @@ -39,16 +35,16 @@ Translations are done externally via Weblate. See [our website](https://aniyomi. # Forks -Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE). +Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/aniyomiorg/aniyomi/blob/main/LICENSE). When creating a fork, remember to: - To avoid confusion with the main app: - Change the app name - Change the app icon - - Change or disable the [app update checker](https://github.com/aniyomiorg/aniyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) + - Change or disable the [app update checker](https://github.com/aniyomiorg/aniyomi/blob/main/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) - To avoid installation conflicts: - - Change the `applicationId` in [`build.gradle.kts`](https://github.com/aniyomiorg/aniyomi/blob/master/app/build.gradle.kts) + - Change the `applicationId` in [`build.gradle.kts`](https://github.com/aniyomiorg/aniyomi/blob/main/app/build.gradle.kts) - To avoid having your data polluting the main app's analytics and crash report services: - - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/aniyomiorg/aniyomi/blob/master/app/src/standard/google-services.json) with your own - - If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/aniyomiorg/aniyomi/blob/master/app/build.gradle.kts) with your own + - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/aniyomiorg/aniyomi/blob/main/app/src/standard/google-services.json) with your own + - If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/aniyomiorg/aniyomi/blob/main/app/build.gradle.kts) with your own diff --git a/README.md b/README.md index ab36a21f99..bbd46ceca9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Discover and watch anime, cartoons, series, and more – easier than ever on you * Local reading and watching of content. * A configurable reader with multiple viewers, reading directions and other settings. * A configurable player built on mpv-android with multiple options and settings. -* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Simkl](https://simkl.com/), and [Bangumi](https://bgm.tv/) support. +* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Simkl](https://simkl.com/), and [Bangumi](https://bgm.tv/) support. * Categories to organize your library. * Light and dark themes. * Schedule updating your library for new chapters/episodes. diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 90c0dd56f7..0000000000 --- a/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/build -*iml -*.iml \ No newline at end of file diff --git a/app/.idea/discord.xml b/app/.idea/discord.xml index d8e9561668..e016cd84e7 100644 --- a/app/.idea/discord.xml +++ b/app/.idea/discord.xml @@ -4,4 +4,4 @@ - \ No newline at end of file + diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml index 6dc906734c..3fca0de45e 100644 --- a/app/.idea/misc.xml +++ b/app/.idea/misc.xml @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/app/.idea/vcs.xml b/app/.idea/vcs.xml index 6c0b863585..54e4b961ee 100644 --- a/app/.idea/vcs.xml +++ b/app/.idea/vcs.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce29ee60b7..f2a5719294 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,19 +2,18 @@ import mihon.buildlogic.getBuildTime import mihon.buildlogic.getCommitCount import mihon.buildlogic.getGitSha import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import java.io.FileInputStream -import java.util.Properties plugins { id("mihon.android.application") id("mihon.android.application.compose") - id("com.mikepenz.aboutlibraries.plugin") id("com.github.zellius.shortcut-helper") kotlin("plugin.serialization") + alias(libs.plugins.aboutLibraries) } shortcutHelper.setFilePath("./shortcuts.xml") +@Suppress("PropertyName") val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { @@ -23,7 +22,7 @@ android { defaultConfig { applicationId = "xyz.jmir.tachiyomi.mi" - versionCode = 125 + versionCode = 128 versionName = "0.16.4.3" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") @@ -115,13 +114,16 @@ android { packaging { resources.excludes.addAll( listOf( + "kotlin-tooling-metadata.json", "META-INF/DEPENDENCIES", "LICENSE.txt", "META-INF/LICENSE", - "META-INF/LICENSE.txt", + "META-INF/**/LICENSE.txt", + "META-INF/*.properties", + "META-INF/**/*.properties", "META-INF/README.md", "META-INF/NOTICE", - "META-INF/*.kotlin_module", + "META-INF/*.version", ), ) } @@ -148,6 +150,7 @@ android { dependencies { implementation(projects.i18n) + implementation(projects.core.archive) implementation(projects.core.common) implementation(projects.coreMetadata) implementation(projects.sourceApi) @@ -167,7 +170,6 @@ dependencies { debugImplementation(compose.ui.tooling) implementation(compose.ui.tooling.preview) implementation(compose.ui.util) - implementation(compose.accompanist.systemuicontroller) implementation(androidx.interpolator) @@ -187,6 +189,7 @@ dependencies { implementation(androidx.appcompat) implementation(androidx.biometricktx) implementation(androidx.constraintlayout) + implementation(androidx.compose.constraintlayout) implementation(androidx.corektx) implementation(androidx.splashscreen) implementation(androidx.recyclerview) @@ -247,7 +250,6 @@ dependencies { implementation(libs.compose.webview) implementation(libs.compose.grid) - // Logging implementation(libs.logcat) diff --git a/app/src/debug/res/mipmap/ic_launcher.xml b/app/src/debug/res/mipmap/ic_launcher.xml index dcbbdf1218..4f4ecc0d2e 100644 --- a/app/src/debug/res/mipmap/ic_launcher.xml +++ b/app/src/debug/res/mipmap/ic_launcher.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/debug/res/mipmap/ic_launcher_round.xml b/app/src/debug/res/mipmap/ic_launcher_round.xml index dcbbdf1218..4f4ecc0d2e 100644 --- a/app/src/debug/res/mipmap/ic_launcher_round.xml +++ b/app/src/debug/res/mipmap/ic_launcher_round.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9458212ceb..64579fd7a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -193,13 +193,13 @@ + android:supportsPictureInPicture="true" + android:theme="@style/Theme.Tachiyomi"> diff --git a/app/src/main/assets/aniyomi.lua b/app/src/main/assets/aniyomi.lua new file mode 100644 index 0000000000..4654e2eac1 --- /dev/null +++ b/app/src/main/assets/aniyomi.lua @@ -0,0 +1,90 @@ +aniyomi = {} +-- UI +function aniyomi.show_text(text) + mp.set_property("user-data/aniyomi/show_text", text) +end +function aniyomi.hide_ui() + mp.set_property("user-data/aniyomi/toggle_ui", "hide") +end +function aniyomi.show_ui() + mp.set_property("user-data/aniyomi/toggle_ui", "show") +end +function aniyomi.toggle_ui() + mp.set_property("user-data/aniyomi/toggle_ui", "toggle") +end +function aniyomi.show_subtitle_settings() + mp.set_property("user-data/aniyomi/show_panel", "subtitle_settings") +end +function aniyomi.show_subtitle_delay() + mp.set_property("user-data/aniyomi/show_panel", "subtitle_delay") +end +function aniyomi.show_audio_delay() + mp.set_property("user-data/aniyomi/show_panel", "audio_delay") +end +function aniyomi.show_video_filters() + mp.set_property("user-data/aniyomi/show_panel", "video_filters") +end +function aniyomi.show_software_keyboard() + mp.set_property("user-data/aniyomi/software_keyboard", "show") +end +function aniyomi.hide_software_keyboard() + mp.set_property("user-data/aniyomi/software_keyboard", "hide") +end +function aniyomi.toggle_software_keyboard() + mp.set_property("user-data/aniyomi/software_keyboard", "toggle") +end +-- Custom buttons +function aniyomi.set_button_title(text) + mp.set_property("user-data/aniyomi/set_button_title", text) +end +function aniyomi.reset_button_title() + mp.set_property("user-data/aniyomi/reset_button_title", "unused") +end +function aniyomi.hide_button() + mp.set_property("user-data/aniyomi/toggle_button", "hide") +end +function aniyomi.show_button() + mp.set_property("user-data/aniyomi/toggle_button", "show") +end +function aniyomi.toggle_button() + mp.set_property("user-data/aniyomi/toggle_button", "toggle") +end +-- Controls +function aniyomi.previous_episode() + mp.set_property("user-data/aniyomi/switch_episode", "p") +end +function aniyomi.next_episode() + mp.set_property("user-data/aniyomi/switch_episode", "n") +end +function aniyomi.pause() + mp.set_property("user-data/aniyomi/pause", "pause") +end +function aniyomi.unpause() + mp.set_property("user-data/aniyomi/pause", "unpause") +end +function aniyomi.pauseunpause() + mp.set_property("user-data/aniyomi/pause", "pauseunpause") +end +function aniyomi.seek_by(value) + mp.set_property("user-data/aniyomi/seek_by", value) +end +function aniyomi.seek_to(value) + mp.set_property("user-data/aniyomi/seek_to", value) +end +function aniyomi.seek_by_with_text(value, text) + mp.set_property("user-data/aniyomi/seek_by_with_text", value .. "|" .. text) +end +function aniyomi.seek_to_with_text(value, text) + mp.set_property("user-data/aniyomi/seek_to_with_text", value .. "|" .. text) +end +function aniyomi.int_picker(title, name_format, start, stop, step, property) + mp.set_property("user-data/aniyomi/launch_int_picker", title .. "|" .. name_format .. "|" .. start .. "|" .. stop .. "|" .. step .. "|" .. property) +end +-- Legacy +function aniyomi.left_seek_by(value) + aniyomi.seek_by(-value) +end +function aniyomi.right_seek_by(value) + aniyomi.seek_by(value) +end +return aniyomi diff --git a/app/src/main/java/aniyomi/util/DataSaver.kt b/app/src/main/java/aniyomi/util/DataSaver.kt index 7aa0860fae..dd8a1fbfbc 100644 --- a/app/src/main/java/aniyomi/util/DataSaver.kt +++ b/app/src/main/java/aniyomi/util/DataSaver.kt @@ -112,7 +112,8 @@ private class WsrvNlDataSaver(preferences: SourcePreferences) : DataSaver { private fun getUrl(imageUrl: String): String { // Network Request sent to wsrv - return "https://wsrv.nl/?url=$imageUrl" + if (imageUrl.contains(".webp", true) || imageUrl.contains( + return "https://wsrv.nl/?url=$imageUrl" + if (imageUrl.contains(".webp", true) || + imageUrl.contains( ".gif", true, ) diff --git a/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt b/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt index 8dab8a054c..baf369e393 100644 --- a/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt +++ b/app/src/main/java/eu/kanade/core/util/CollectionUtils.kt @@ -5,7 +5,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract fun List.insertSeparators( - generator: (T?, T?) -> R?, + generator: (before: T?, after: T?) -> R?, ): List { if (isEmpty()) return emptyList() val newList = mutableListOf() @@ -19,6 +19,24 @@ fun List.insertSeparators( return newList } +/** + * Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element + */ +fun List.insertSeparatorsReversed( + generator: (before: T?, after: T?) -> R?, +): List { + if (isEmpty()) return emptyList() + val newList = mutableListOf() + for (i in size downTo 0) { + val after = getOrNull(i) + after?.let(newList::add) + val before = getOrNull(i - 1) + val separator = generator.invoke(before, after) + separator?.let(newList::add) + } + return newList.asReversed() +} + fun HashSet.addOrRemove(value: E, shouldAdd: Boolean) { if (shouldAdd) { add(value) diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index c54db27683..f4bf0b2e6d 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -41,6 +41,7 @@ import eu.kanade.domain.track.manga.interactor.AddMangaTracks import eu.kanade.domain.track.manga.interactor.RefreshMangaTracks import eu.kanade.domain.track.manga.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.manga.interactor.TrackChapter +import eu.kanade.tachiyomi.ui.player.utils.TrackSelect import mihon.data.repository.anime.AnimeExtensionRepoRepositoryImpl import mihon.data.repository.manga.MangaExtensionRepoRepositoryImpl import mihon.domain.extensionrepo.anime.interactor.CreateAnimeExtensionRepo @@ -58,10 +59,13 @@ import mihon.domain.extensionrepo.manga.interactor.ReplaceMangaExtensionRepo import mihon.domain.extensionrepo.manga.interactor.UpdateMangaExtensionRepo import mihon.domain.extensionrepo.manga.repository.MangaExtensionRepoRepository import mihon.domain.extensionrepo.service.ExtensionRepoService +import mihon.domain.items.chapter.interactor.FilterChaptersForDownload +import mihon.domain.items.episode.interactor.FilterEpisodesForDownload import mihon.domain.upcoming.anime.interactor.GetUpcomingAnime import mihon.domain.upcoming.manga.interactor.GetUpcomingManga import tachiyomi.data.category.anime.AnimeCategoryRepositoryImpl import tachiyomi.data.category.manga.MangaCategoryRepositoryImpl +import tachiyomi.data.custombutton.CustomButtonRepositoryImpl import tachiyomi.data.entries.anime.AnimeRepositoryImpl import tachiyomi.data.entries.manga.MangaRepositoryImpl import tachiyomi.data.history.anime.AnimeHistoryRepositoryImpl @@ -103,6 +107,13 @@ import tachiyomi.domain.category.manga.interactor.SetMangaDisplayMode import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory import tachiyomi.domain.category.manga.repository.MangaCategoryRepository +import tachiyomi.domain.custombuttons.interactor.CreateCustomButton +import tachiyomi.domain.custombuttons.interactor.DeleteCustomButton +import tachiyomi.domain.custombuttons.interactor.GetCustomButtons +import tachiyomi.domain.custombuttons.interactor.ReorderCustomButton +import tachiyomi.domain.custombuttons.interactor.ToggleFavoriteCustomButton +import tachiyomi.domain.custombuttons.interactor.UpdateCustomButton +import tachiyomi.domain.custombuttons.repository.CustomButtonRepository import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId @@ -284,6 +295,7 @@ class DomainModule : InjektModule { addFactory { SetSeenStatus(get(), get(), get(), get()) } addFactory { ShouldUpdateDbEpisode() } addFactory { SyncEpisodesWithSource(get(), get(), get(), get(), get(), get(), get()) } + addFactory { FilterEpisodesForDownload(get(), get(), get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } @@ -294,6 +306,7 @@ class DomainModule : InjektModule { addFactory { ShouldUpdateDbChapter() } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { GetAvailableScanlators(get()) } + addFactory { FilterChaptersForDownload(get(), get(), get()) } addSingletonFactory { AnimeHistoryRepositoryImpl(get()) } addFactory { GetAnimeHistory(get()) } @@ -365,5 +378,15 @@ class DomainModule : InjektModule { addFactory { DeleteMangaExtensionRepo(get()) } addFactory { ReplaceMangaExtensionRepo(get()) } addFactory { UpdateMangaExtensionRepo(get(), get()) } + + addSingletonFactory { CustomButtonRepositoryImpl(get()) } + addFactory { CreateCustomButton(get()) } + addFactory { DeleteCustomButton(get()) } + addFactory { GetCustomButtons(get()) } + addFactory { UpdateCustomButton(get()) } + addFactory { ReorderCustomButton(get()) } + addFactory { ToggleFavoriteCustomButton(get()) } + + addFactory { TrackSelect(get(), get()) } } } diff --git a/app/src/main/java/eu/kanade/domain/entries/anime/interactor/SetAnimeViewerFlags.kt b/app/src/main/java/eu/kanade/domain/entries/anime/interactor/SetAnimeViewerFlags.kt index 2937f867a9..a4edc9f965 100644 --- a/app/src/main/java/eu/kanade/domain/entries/anime/interactor/SetAnimeViewerFlags.kt +++ b/app/src/main/java/eu/kanade/domain/entries/anime/interactor/SetAnimeViewerFlags.kt @@ -1,5 +1,6 @@ package eu.kanade.domain.entries.anime.interactor +import tachiyomi.core.common.util.lang.toLong import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.repository.AnimeRepository @@ -14,7 +15,10 @@ class SetAnimeViewerFlags( animeRepository.updateAnime( AnimeUpdate( id = id, - viewerFlags = anime.viewerFlags.setFlag(flag, Anime.ANIME_INTRO_MASK), + viewerFlags = anime.viewerFlags + .setFlag(flag, Anime.ANIME_INTRO_MASK) + // Disable skip intro button if length is set to 0 + .setFlag((flag == 0L).toLong().addHexZeros(14), Anime.ANIME_INTRO_DISABLE_MASK), ), ) } diff --git a/app/src/main/java/eu/kanade/domain/track/anime/interactor/AddAnimeTracks.kt b/app/src/main/java/eu/kanade/domain/track/anime/interactor/AddAnimeTracks.kt index 91dd332a09..e401de5b74 100644 --- a/app/src/main/java/eu/kanade/domain/track/anime/interactor/AddAnimeTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/anime/interactor/AddAnimeTracks.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.track.AnimeTracker import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import logcat.LogPriority import tachiyomi.core.common.util.lang.withIOContext @@ -15,17 +16,16 @@ import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.history.anime.interactor.GetAnimeHistory import tachiyomi.domain.items.episode.interactor.GetEpisodesByAnimeId -import tachiyomi.domain.track.anime.interactor.GetAnimeTracks import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.ZoneOffset class AddAnimeTracks( - private val getTracks: GetAnimeTracks, private val insertTrack: InsertAnimeTrack, private val syncChapterProgressWithTrack: SyncEpisodeProgressWithTrack, private val getEpisodesByAnimeId: GetEpisodesByAnimeId, + private val trackerManager: TrackerManager, ) { // TODO: update all trackers based on common data @@ -80,7 +80,7 @@ class AddAnimeTracks( suspend fun bindEnhancedTrackers(anime: Anime, source: AnimeSource) = withNonCancellableContext { withIOContext { - getTracks.await(anime.id) + trackerManager.loggedInTrackers() .filterIsInstance() .filter { it.accept(source) } .forEach { service -> @@ -88,11 +88,11 @@ class AddAnimeTracks( service.match(anime)?.let { track -> track.anime_id = anime.id (service as Tracker).animeService.bind(track) - insertTrack.await(track.toDomainTrack()!!) + insertTrack.await(track.toDomainTrack(idRequired = false)!!) syncChapterProgressWithTrack.await( anime.id, - track.toDomainTrack()!!, + track.toDomainTrack(idRequired = false)!!, service.animeService, ) } diff --git a/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt index cf2c56aeee..f30832bdb1 100644 --- a/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/anime/interactor/SyncEpisodeProgressWithTrack.kt @@ -10,6 +10,7 @@ import tachiyomi.domain.items.episode.interactor.UpdateEpisode import tachiyomi.domain.items.episode.model.toEpisodeUpdate import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack import tachiyomi.domain.track.anime.model.AnimeTrack +import kotlin.math.max class SyncEpisodeProgressWithTrack( private val updateEpisode: UpdateEpisode, @@ -36,7 +37,8 @@ class SyncEpisodeProgressWithTrack( // only take into account continuous watching val localLastSeen = sortedEpisodes.takeWhile { it.seen }.lastOrNull()?.episodeNumber ?: 0F - val updatedTrack = remoteTrack.copy(lastEpisodeSeen = localLastSeen.toDouble()) + val lastSeen = max(remoteTrack.lastEpisodeSeen, localLastSeen.toDouble()) + val updatedTrack = remoteTrack.copy(lastEpisodeSeen = lastSeen) try { service.update(updatedTrack.toDbTrack()) diff --git a/app/src/main/java/eu/kanade/domain/track/manga/interactor/AddMangaTracks.kt b/app/src/main/java/eu/kanade/domain/track/manga/interactor/AddMangaTracks.kt index fdcd7fba36..aa28f4b70e 100644 --- a/app/src/main/java/eu/kanade/domain/track/manga/interactor/AddMangaTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/manga/interactor/AddMangaTracks.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.EnhancedMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import logcat.LogPriority @@ -15,17 +16,16 @@ import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.history.manga.interactor.GetMangaHistory import tachiyomi.domain.items.chapter.interactor.GetChaptersByMangaId -import tachiyomi.domain.track.manga.interactor.GetMangaTracks import tachiyomi.domain.track.manga.interactor.InsertMangaTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.ZoneOffset class AddMangaTracks( - private val getTracks: GetMangaTracks, private val insertTrack: InsertMangaTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, private val getChaptersByMangaId: GetChaptersByMangaId, + private val trackerManager: TrackerManager, ) { // TODO: update all trackers based on common data @@ -80,7 +80,7 @@ class AddMangaTracks( suspend fun bindEnhancedTrackers(manga: Manga, source: MangaSource) = withNonCancellableContext { withIOContext { - getTracks.await(manga.id) + trackerManager.loggedInTrackers() .filterIsInstance() .filter { it.accept(source) } .forEach { service -> @@ -88,11 +88,11 @@ class AddMangaTracks( service.match(manga)?.let { track -> track.manga_id = manga.id (service as Tracker).mangaService.bind(track) - insertTrack.await(track.toDomainTrack()!!) + insertTrack.await(track.toDomainTrack(idRequired = false)!!) syncChapterProgressWithTrack.await( manga.id, - track.toDomainTrack()!!, + track.toDomainTrack(idRequired = false)!!, service.mangaService, ) } diff --git a/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt index ab36ce46f0..2ce23160d1 100644 --- a/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/manga/interactor/SyncChapterProgressWithTrack.kt @@ -10,6 +10,7 @@ import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.model.toChapterUpdate import tachiyomi.domain.track.manga.interactor.InsertMangaTrack import tachiyomi.domain.track.manga.model.MangaTrack +import kotlin.math.max class SyncChapterProgressWithTrack( private val updateChapter: UpdateChapter, @@ -36,7 +37,8 @@ class SyncChapterProgressWithTrack( // only take into account continuous reading val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F - val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble()) + val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble()) + val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead) try { tracker.update(updatedTrack.toDbTrack()) diff --git a/app/src/main/java/eu/kanade/domain/track/model/AutoTrackState.kt b/app/src/main/java/eu/kanade/domain/track/model/AutoTrackState.kt new file mode 100644 index 0000000000..987999b749 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/track/model/AutoTrackState.kt @@ -0,0 +1,10 @@ +package eu.kanade.domain.track.model + +import dev.icerock.moko.resources.StringResource +import tachiyomi.i18n.MR + +enum class AutoTrackState(val titleRes: StringResource) { + ALWAYS(MR.strings.lock_always), + ASK(MR.strings.default_category_summary), + NEVER(MR.strings.lock_never), +} diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt index 81b3f01d1d..f6222d1dc6 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt @@ -1,9 +1,11 @@ package eu.kanade.domain.track.service +import eu.kanade.domain.track.model.AutoTrackState import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.anilist.Anilist import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore +import tachiyomi.core.common.preference.getEnum class TrackPreferences( private val preferenceStore: PreferenceStore, @@ -42,4 +44,9 @@ class TrackPreferences( "show_next_episode_airing_time", true, ) + + fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum( + "pref_auto_update_manga_on_mark_read", + AutoTrackState.ALWAYS, + ) } diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index e54886b456..7496cf6e88 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -21,7 +21,11 @@ class UiPreferences( fun appTheme() = preferenceStore.getEnum( "pref_app_theme", - if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT }, + if (DeviceUtil.isDynamicColorAvailable) { + AppTheme.MONET + } else { + AppTheme.DEFAULT + }, ) fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) diff --git a/app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt b/app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt index ef4558922c..6ba53e65a7 100644 --- a/app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt +++ b/app/src/main/java/eu/kanade/domain/ui/model/NavStyle.kt @@ -25,7 +25,7 @@ enum class NavStyle( MOVE_MANGA_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_manga, moreTab = MangaLibraryTab), MOVE_UPDATES_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_updates, moreTab = UpdatesTab), MOVE_HISTORY_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_history, moreTab = HistoriesTab), - MOVE_BROWSE_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_browse, moreTab = BrowseTab()), + MOVE_BROWSE_TO_MORE(titleRes = MR.strings.pref_bottom_nav_no_browse, moreTab = BrowseTab), ; val moreIcon: ImageVector @@ -44,7 +44,7 @@ enum class NavStyle( MangaLibraryTab, UpdatesTab, HistoriesTab, - BrowseTab(), + BrowseTab, MoreTab, ).apply { remove(this@NavStyle.moreTab) } } diff --git a/app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt b/app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt index a41ead2bc1..c5e16fddad 100644 --- a/app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt +++ b/app/src/main/java/eu/kanade/domain/ui/model/StartScreen.kt @@ -14,5 +14,5 @@ enum class StartScreen(val titleRes: StringResource, val tab: Tab) { MANGA(MR.strings.manga, MangaLibraryTab), UPDATES(MR.strings.label_recent_updates, UpdatesTab), HISTORY(MR.strings.label_recent_manga, HistoriesTab), - BROWSE(MR.strings.browse, BrowseTab()), + BROWSE(MR.strings.browse, BrowseTab), } diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionDetailsScreen.kt index 0ed3010298..b8d9f181d2 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionDetailsScreen.kt @@ -241,7 +241,7 @@ private fun DetailsHeader( Update available: ${extension.hasUpdate} Obsolete: ${extension.isObsolete} Shared: ${extension.isShared} - Repository: ${extension.repoUrl} + Repository: ${extension.repoUrl} """.trimIndent(), ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionsScreen.kt index bc0ae578dd..68c61d0870 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeExtensionsScreen.kt @@ -47,6 +47,7 @@ import eu.kanade.presentation.browse.manga.ExtensionTrustDialog import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScreen +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension @@ -185,14 +186,14 @@ private fun AnimeExtensionContent( } ExtensionHeader( textRes = header.textRes, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), action = action, ) } is AnimeExtensionUiModel.Header.Text -> { ExtensionHeader( text = header.text, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), ) } } @@ -211,12 +212,14 @@ private fun AnimeExtensionContent( ) { item -> AnimeExtensionItem( item = item, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), onClickItem = { when (it) { is AnimeExtension.Available -> onInstallExtension(it) is AnimeExtension.Installed -> onOpenExtension(it) - is AnimeExtension.Untrusted -> { trustState = it } + is AnimeExtension.Untrusted -> { + trustState = it + } } }, onLongClickItem = onLongClickItem, diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt index 8aafc1a4e2..538979c401 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesFilterScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesFilterScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import tachiyomi.domain.source.anime.model.AnimeSource @@ -68,7 +69,7 @@ private fun AnimeSourcesFilterContent( contentType = "source-filter-header", ) { AnimeSourcesFilterHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), language = language, enabled = enabled, onClickItem = onClickLanguage, @@ -81,7 +82,7 @@ private fun AnimeSourcesFilterContent( contentType = { "source-filter-item" }, ) { source -> AnimeSourcesFilterItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), source = source, isEnabled = "${source.id}" !in state.disabledSources, onClickItem = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesScreen.kt index d2ce30aa4f..4ce0bd5558 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/AnimeSourcesScreen.kt @@ -28,7 +28,7 @@ import tachiyomi.domain.source.anime.model.AnimeSource import tachiyomi.domain.source.anime.model.Pin import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ScrollbarLazyColumn -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.i18n.stringResource @@ -151,7 +151,7 @@ private fun AnimeSourcePinButton( MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.onBackground.copy( - alpha = SecondaryItemAlpha, + alpha = SECONDARY_ALPHA, ) } val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt index 5e66204b66..1273d0f77a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/BrowseAnimeSourceScreen.kt @@ -41,6 +41,8 @@ fun BrowseAnimeSourceContent( source: AnimeSource?, animeList: LazyPagingItems>, columns: GridCells, + entries: Int = 0, + topBarHeight: Int = 0, displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, @@ -129,6 +131,8 @@ fun BrowseAnimeSourceContent( LibraryDisplayMode.List -> { BrowseAnimeSourceList( animeList = animeList, + entries = entries, + topBarHeight = topBarHeight, contentPadding = contentPadding, onAnimeClick = onAnimeClick, onAnimeLongClick = onAnimeLongClick, diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeIcons.kt index 4304ba0af8..62e903a622 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeIcons.kt @@ -130,7 +130,7 @@ private fun AnimeExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT val appInfo = AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName( context, pkgName, - )!!.applicationInfo + )!!.applicationInfo!! val appResources = context.packageManager.getResourcesForApplication(appInfo) Result.Success( appResources.getDrawableForDensity(appInfo.icon, density, null)!! diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt index c9a215f187..011fd6047e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceList.kt @@ -5,6 +5,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems @@ -20,12 +25,19 @@ import tachiyomi.presentation.core.util.plus @Composable fun BrowseAnimeSourceList( animeList: LazyPagingItems>, + entries: Int, + topBarHeight: Int, contentPadding: PaddingValues, onAnimeClick: (Anime) -> Unit, onAnimeLongClick: (Anime) -> Unit, ) { + var containerHeight by remember { mutableIntStateOf(0) } LazyColumn( contentPadding = contentPadding + PaddingValues(vertical = 8.dp), + modifier = Modifier + .onGloballyPositioned { layoutCoordinates -> + containerHeight = layoutCoordinates.size.height - topBarHeight + }, ) { item { if (animeList.loadState.prepend is LoadState.Loading) { @@ -39,6 +51,8 @@ fun BrowseAnimeSourceList( anime = anime, onClick = { onAnimeClick(anime) }, onLongClick = { onAnimeLongClick(anime) }, + entries = entries, + containerHeight = containerHeight, ) } @@ -55,6 +69,8 @@ private fun BrowseAnimeSourceListItem( anime: Anime, onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, + entries: Int, + containerHeight: Int, ) { EntryListItem( title = anime.title, @@ -71,5 +87,7 @@ private fun BrowseAnimeSourceListItem( }, onLongClick = onLongClick, onClick = onClick, + entries = entries, + containerHeight = containerHeight, ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt index 694f926612..030351287f 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt @@ -41,6 +41,8 @@ fun BrowseSourceContent( source: MangaSource?, mangaList: LazyPagingItems>, columns: GridCells, + entries: Int = 0, + topBarHeight: Int = 0, displayMode: LibraryDisplayMode, snackbarHostState: SnackbarHostState, contentPadding: PaddingValues, @@ -129,6 +131,8 @@ fun BrowseSourceContent( LibraryDisplayMode.List -> { BrowseMangaSourceList( mangaList = mangaList, + entries = entries, + topBarHeight = topBarHeight, contentPadding = contentPadding, onMangaClick = onMangaClick, onMangaLongClick = onMangaLongClick, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionDetailsScreen.kt index 92423923c6..d0c3557612 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionDetailsScreen.kt @@ -242,7 +242,7 @@ private fun DetailsHeader( Update available: ${extension.hasUpdate} Obsolete: ${extension.isObsolete} Shared: ${extension.isShared} - Repository: ${extension.repoUrl} + Repository: ${extension.repoUrl} """.trimIndent(), ) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionsScreen.kt index f3862c14c6..55584f526f 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaExtensionsScreen.kt @@ -48,6 +48,7 @@ import eu.kanade.presentation.browse.manga.components.MangaExtensionIcon import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.entries.components.DotSeparatorNoSpaceText import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScreen +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.manga.model.MangaExtension @@ -187,14 +188,14 @@ private fun ExtensionContent( } ExtensionHeader( textRes = header.textRes, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), action = action, ) } is MangaExtensionUiModel.Header.Text -> { ExtensionHeader( text = header.text, - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), ) } } @@ -212,13 +213,15 @@ private fun ExtensionContent( }, ) { item -> ExtensionItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), item = item, onClickItem = { when (it) { is MangaExtension.Available -> onInstallExtension(it) is MangaExtension.Installed -> onOpenExtension(it) - is MangaExtension.Untrusted -> { trustState = it } + is MangaExtension.Untrusted -> { + trustState = it + } } }, onLongClickItem = onLongClickItem, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt index b61f9c4106..4d860ed2b4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesFilterScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesFilterScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import tachiyomi.domain.source.manga.model.Source @@ -68,7 +69,7 @@ private fun SourcesFilterContent( contentType = "source-filter-header", ) { SourcesFilterHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), language = language, enabled = enabled, onClickItem = onClickLanguage, @@ -81,7 +82,7 @@ private fun SourcesFilterContent( contentType = { "source-filter-item" }, ) { source -> SourcesFilterItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), source = source, enabled = "${source.id}" !in state.disabledSources, onClickItem = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt index 8640bf6bc8..978fcf2441 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt @@ -28,7 +28,7 @@ import tachiyomi.domain.source.manga.model.Pin import tachiyomi.domain.source.manga.model.Source import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ScrollbarLazyColumn -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.i18n.stringResource @@ -151,7 +151,7 @@ private fun SourcePinButton( MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.onBackground.copy( - alpha = SecondaryItemAlpha, + alpha = SECONDARY_ALPHA, ) } val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt index 8dfc933f46..954a4d430d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt @@ -130,7 +130,7 @@ private fun MangaExtension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT val appInfo = MangaExtensionLoader.getMangaExtensionPackageInfoFromPkgName( context, pkgName, - )!!.applicationInfo + )!!.applicationInfo!! val appResources = context.packageManager.getResourcesForApplication(appInfo) Result.Success( appResources.getDrawableForDensity(appInfo.icon, density, null)!! diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt index 0cbaa55824..6d75143e3b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceList.kt @@ -5,6 +5,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems @@ -19,12 +24,19 @@ import tachiyomi.presentation.core.util.plus @Composable fun BrowseMangaSourceList( mangaList: LazyPagingItems>, + entries: Int, + topBarHeight: Int, contentPadding: PaddingValues, onMangaClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit, ) { + var containerHeight by remember { mutableIntStateOf(0) } LazyColumn( contentPadding = contentPadding + PaddingValues(vertical = 8.dp), + modifier = Modifier + .onGloballyPositioned { layoutCoordinates -> + containerHeight = layoutCoordinates.size.height - topBarHeight + }, ) { item { if (mangaList.loadState.prepend is LoadState.Loading) { @@ -38,6 +50,8 @@ fun BrowseMangaSourceList( manga = manga, onClick = { onMangaClick(manga) }, onLongClick = { onMangaLongClick(manga) }, + entries = entries, + containerHeight = containerHeight, ) } @@ -54,6 +68,8 @@ private fun BrowseMangaSourceListItem( manga: Manga, onClick: () -> Unit = {}, onLongClick: () -> Unit = onClick, + entries: Int, + containerHeight: Int, ) { EntryListItem( title = manga.title, @@ -70,5 +86,7 @@ private fun BrowseMangaSourceListItem( }, onLongClick = onLongClick, onClick = onClick, + entries = entries, + containerHeight = containerHeight, ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index 30ae43a43d..15d05a6ec6 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -9,12 +9,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.core.view.WindowInsetsControllerCompat import cafe.adriel.voyager.core.annotation.InternalVoyagerApi import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator -import com.google.accompanist.systemuicontroller.rememberSystemUiController import eu.kanade.presentation.util.ScreenTransition import eu.kanade.presentation.util.isTabletUi import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl @@ -71,7 +69,6 @@ fun NavigatorAdaptiveSheet( fun AdaptiveSheet( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, - hideSystemBars: Boolean = false, enableSwipeDismiss: Boolean = true, content: @Composable () -> Unit, ) { @@ -81,12 +78,6 @@ fun AdaptiveSheet( onDismissRequest = onDismissRequest, properties = dialogProperties, ) { - if (hideSystemBars) { - rememberSystemUiController().apply { - isSystemBarsVisible = false - systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } AdaptiveSheetImpl( modifier = modifier, isTabletUi = isTabletUi, diff --git a/app/src/main/java/eu/kanade/presentation/components/DateText.kt b/app/src/main/java/eu/kanade/presentation/components/DateText.kt index 17f95bbd89..030708f3c3 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DateText.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DateText.kt @@ -27,6 +27,7 @@ fun relativeDateText( ) } +// For use in chapter/episode release time @Composable fun relativeDateTimeText( dateEpochMillis: Long, @@ -58,6 +59,7 @@ fun relativeDateText( ?: stringResource(MR.strings.not_applicable) } +// For use in chapter/episode release time @Composable fun relativeDateTimeText( localDateTime: LocalDateTime?, diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index b9f7f0e204..b86057a67a 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -47,12 +47,10 @@ fun TabbedDialog( tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null, onOverflowMenuClicked: (() -> Unit)? = null, overflowIcon: ImageVector? = null, - hideSystemBars: Boolean = false, pagerState: PagerState = rememberPagerState { tabTitles.size }, content: @Composable (Int) -> Unit, ) { AdaptiveSheet( - hideSystemBars = hideSystemBars, modifier = modifier, onDismissRequest = onDismissRequest, ) { diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index ace5dbda7f..36113687cd 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow @@ -15,7 +16,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Tab import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -36,7 +36,7 @@ fun TabbedScreen( titleRes: StringResource?, tabs: ImmutableList, modifier: Modifier = Modifier, - startIndex: Int? = null, + state: PagerState = rememberPagerState { tabs.size }, mangaSearchQuery: String? = null, onChangeMangaSearchQuery: (String?) -> Unit = {}, scrollable: Boolean = false, @@ -45,15 +45,8 @@ fun TabbedScreen( ) { val scope = rememberCoroutineScope() - val state = rememberPagerState { tabs.size } val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(startIndex) { - if (startIndex != null) { - state.scrollToPage(startIndex) - } - } - Scaffold( topBar = { if (titleRes != null) { diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt index 48f6929d44..4e913aa7d0 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/AnimeScreen.kt @@ -427,13 +427,9 @@ private fun AnimeScreenSmallImpl( AnimeInfoBox( isTabletUi = false, appBarPadding = topPadding, - title = state.anime.title, - author = state.anime.author, - artist = state.anime.artist, + anime = state.anime, sourceName = remember { state.source.getNameForAnimeInfo() }, isStubSource = remember { state.source is StubAnimeSource }, - coverDataProvider = { state.anime }, - status = state.anime.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) @@ -499,7 +495,8 @@ private fun AnimeScreenSmallImpl( timer -= 1000L } } - if (timer > 0L && showNextEpisodeAirTime && + if (timer > 0L && + showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED ) { NextEpisodeAiringListItem( @@ -708,13 +705,9 @@ fun AnimeScreenLargeImpl( AnimeInfoBox( isTabletUi = true, appBarPadding = contentPadding.calculateTopPadding(), - title = state.anime.title, - author = state.anime.author, - artist = state.anime.artist, + anime = state.anime, sourceName = remember { state.source.getNameForAnimeInfo() }, isStubSource = remember { state.source is StubAnimeSource }, - coverDataProvider = { state.anime }, - status = state.anime.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) @@ -781,7 +774,8 @@ fun AnimeScreenLargeImpl( timer -= 1000L } } - if (timer > 0L && showNextEpisodeAirTime && + if (timer > 0L && + showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED ) { NextEpisodeAiringListItem( diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt index f671671c5b..edc53f62cf 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/EpisodeOptionsDialogScreen.kt @@ -6,11 +6,13 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars @@ -29,13 +31,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -46,19 +44,31 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.tachiyomi.animesource.AnimeSource +import eu.kanade.tachiyomi.animesource.model.Hoster import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.player.controls.components.sheets.HosterState +import eu.kanade.tachiyomi.ui.player.controls.components.sheets.QualitySheetHosterContent +import eu.kanade.tachiyomi.ui.player.controls.components.sheets.QualitySheetVideoContent +import eu.kanade.tachiyomi.ui.player.controls.components.sheets.getChangedAt import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader +import eu.kanade.tachiyomi.ui.player.loader.HosterLoader import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -73,6 +83,8 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.cancellation.CancellationException class EpisodeOptionsDialogScreen( private val useExternalDownloader: Boolean, @@ -91,14 +103,29 @@ class EpisodeOptionsDialogScreen( sourceId = sourceId, ) } - val state by sm.state.collectAsState() + + val episode by sm.episode.collectAsState() + val anime by sm.anime.collectAsState() + val hosterState by sm.hosterState.collectAsState() + val hosterExpandedList by sm.hosterExpandedList.collectAsState() + val selectedHosterVideoIndex by sm.selectedHosterVideoIndex.collectAsState() + val currentVideo by sm.currentVideo.collectAsState() + val showAllQualities by sm.showAllQualities.collectAsState() EpisodeOptionsDialog( useExternalDownloader = useExternalDownloader, episodeTitle = episodeTitle, - episode = state.episode, - anime = state.anime, - resultList = state.resultList, + episode = episode, + anime = anime, + showAllQualities = showAllQualities, + resultList = hosterState, + expandedList = hosterExpandedList, + currentVideo = currentVideo, + selectedHosterVideoIndex = selectedHosterVideoIndex, + onShowAllQualities = sm::onShowAllQualities, + onClickHoster = sm::onClickHoster, + onClickVideo = sm::onClickVideo, + getHosterList = sm::getHosterList, ) } @@ -111,38 +138,256 @@ class EpisodeOptionsDialogScreenModel( episodeId: Long, animeId: Long, sourceId: Long, -) : StateScreenModel(State()) { +) : ScreenModel { private val sourceManager: AnimeSourceManager = Injekt.get() + private val _hosterState = MutableStateFlow>?>(null) + val hosterState = _hosterState.asStateFlow() + private val _hosterExpandedList = MutableStateFlow>(emptyList()) + val hosterExpandedList = _hosterExpandedList.asStateFlow() + private val _selectedHosterVideoIndex = MutableStateFlow(Pair(-1, -1)) + val selectedHosterVideoIndex = _selectedHosterVideoIndex.asStateFlow() + private val _currentVideo = MutableStateFlow(null) + val currentVideo = _currentVideo.asStateFlow() + + private val _episode = MutableStateFlow(null) + val episode = _episode.asStateFlow() + private val _anime = MutableStateFlow(null) + val anime = _anime.asStateFlow() + + @Suppress("ktlint:standard:backing-property-naming") + private val _hosterList = MutableStateFlow>(emptyList()) + + @Suppress("ktlint:standard:backing-property-naming") + private val _source = MutableStateFlow(null) + + private val _showAllQualities = MutableStateFlow(false) + val showAllQualities = _showAllQualities.asStateFlow() + init { - screenModelScope.launch { - // To show loading state - mutableState.update { it.copy(episode = null, anime = null, resultList = null) } + val hasFoundPreferredVideo = AtomicBoolean(false) + screenModelScope.launchIO { val episode = Injekt.get().await(episodeId)!! val anime = Injekt.get().await(animeId)!! val source = sourceManager.getOrStub(sourceId) - val result = withIOContext { + _episode.update { _ -> episode } + _anime.update { _ -> anime } + _source.update { _ -> source } + + val hosterListResult = withIOContext { try { - val results = EpisodeLoader.getLinks(episode, anime, source) - Result.success(results) - } catch (e: Throwable) { + Result.success(EpisodeLoader.getHosters(episode, anime, source)) + } catch (e: Exception) { Result.failure(e) } } - mutableState.update { it.copy(episode = episode, anime = anime, resultList = result) } + if (hosterListResult.isFailure) { + _hosterState.update { _ -> Result.failure(hosterListResult.exceptionOrNull()!!) } + return@launchIO + } + + val hosterList = hosterListResult.getOrThrow() + _hosterList.update { _ -> hosterList } + _hosterExpandedList.update { _ -> + List(hosterList.size) { true } + } + + val initialHosterState = hosterList.map { hoster -> + if (hoster.videoList == null) { + HosterState.Loading(hoster.hosterName) + } else { + val videoList = hoster.videoList!! + HosterState.Ready( + hoster.hosterName, + videoList, + List(videoList.size) { Video.State.LOAD_VIDEO }, + ) + } + } + + _hosterState.update { _ -> Result.success(initialHosterState) } + + try { + hosterList.mapIndexed { hosterIdx, hoster -> + async { + val hosterState = EpisodeLoader.loadHosterVideos(source, hoster) + + _hosterState.updateAt(hosterIdx, hosterState) + + if (hosterState is HosterState.Ready) { + val prefIndex = hosterState.videoList.indexOfFirst { it.preferred } + if (prefIndex != -1) { + if (hasFoundPreferredVideo.compareAndSet(false, true)) { + val success = + loadVideo(source, hosterState.videoList[prefIndex], hosterIdx, prefIndex) + if (!success) { + hasFoundPreferredVideo.set(false) + } + } + } + } + } + }.awaitAll() + + if (hasFoundPreferredVideo.compareAndSet(false, true)) { + val hosterStateList = hosterState.value!!.getOrThrow() + val (hosterIdx, videoIdx) = HosterLoader.selectBestVideo(hosterStateList) + if (hosterIdx == -1) { + _hosterState.update { _ -> + Result.failure(NoSuchElementException("No available videos")) + } + return@launchIO + } + + val video = (hosterStateList[hosterIdx] as HosterState.Ready).videoList[videoIdx] + + loadVideo(source, video, hosterIdx, videoIdx) + } + } catch (e: CancellationException) { + _hosterState.update { _ -> + Result.success(hosterList.map { HosterState.Idle(it.hosterName) }) + } + + throw e + } } } -} -@Immutable -data class State( - val episode: Episode? = null, - val anime: Anime? = null, - val resultList: Result>? = null, -) + private suspend fun loadVideo(source: AnimeSource, video: Video, hosterIndex: Int, videoIndex: Int): Boolean { + val selectedHosterState = (_hosterState.value!!.getOrThrow()[hosterIndex] as? HosterState.Ready) ?: return false + + val oldSelectedIndex = _selectedHosterVideoIndex.value + _selectedHosterVideoIndex.update { _ -> Pair(hosterIndex, videoIndex) } + + _hosterState.updateAt( + hosterIndex, + selectedHosterState.getChangedAt(videoIndex, video, Video.State.LOAD_VIDEO), + ) + + val resolvedVideo = if (selectedHosterState.videoState[videoIndex] != Video.State.READY) { + HosterLoader.getResolvedVideo(source, video) + } else { + video + } + + if (resolvedVideo == null || resolvedVideo.videoUrl.isEmpty()) { + if (currentVideo.value == null) { + _hosterState.updateAt( + hosterIndex, + selectedHosterState.getChangedAt(videoIndex, video, Video.State.ERROR), + ) + + val hosterStateList = hosterState.value?.getOrNull() ?: return false + + val (newHosterIdx, newVideoIdx) = HosterLoader.selectBestVideo(hosterStateList) + if (newHosterIdx == -1) { + _hosterState.update { _ -> + Result.failure(NoSuchElementException("No available videos")) + } + return false + } + + val newVideo = (hosterStateList[newHosterIdx] as HosterState.Ready).videoList[newVideoIdx] + + return loadVideo(source, newVideo, newHosterIdx, newVideoIdx) + } else { + _selectedHosterVideoIndex.update { _ -> oldSelectedIndex } + _hosterState.updateAt( + hosterIndex, + selectedHosterState.getChangedAt(videoIndex, video, Video.State.ERROR), + ) + return false + } + } + + _hosterState.updateAt( + hosterIndex, + selectedHosterState.getChangedAt(videoIndex, resolvedVideo, Video.State.READY), + ) + _currentVideo.update { _ -> resolvedVideo } + + return true + } + + private fun MutableStateFlow>?>.updateAt(index: Int, newValue: T) { + this.update { values -> + values?.getOrNull()?.let { + Result.success( + it.toMutableList().apply { + this[index] = newValue + }, + ) + } ?: values + } + } + + fun onShowAllQualities(value: Boolean) { + _showAllQualities.update { _ -> value } + } + + fun onClickHoster(hosterIndex: Int) { + val hosterState = hosterState.value?.getOrNull()?.getOrNull(hosterIndex) ?: return + + when (hosterState) { + is HosterState.Ready -> { + _hosterExpandedList.update { values -> + values.toMutableList().apply { + this[hosterIndex] = !hosterExpandedList.value[hosterIndex] + } + } + } + is HosterState.Error, is HosterState.Idle -> { + val hosterName = hosterState.name + _hosterState.updateAt(hosterIndex, HosterState.Loading(hosterName)) + + screenModelScope.launchIO { + val newHosterState = EpisodeLoader.loadHosterVideos( + _source.value!!, + _hosterList.value[hosterIndex], + ) + _hosterState.updateAt(hosterIndex, newHosterState) + } + } + is HosterState.Loading -> {} + } + } + + fun onClickVideo(hosterIndex: Int, videoIndex: Int) { + val video = (_hosterState.value?.getOrNull()?.getOrNull(hosterIndex) as? HosterState.Ready) + ?.videoList + ?.getOrNull(videoIndex) + ?: return + + screenModelScope.launchIO { + val success = loadVideo(_source.value!!, video, hosterIndex, videoIndex) + if (success) { + _showAllQualities.update { _ -> false } + } + } + } + + fun getHosterList(): List? { + val hosterStateList = hosterState.value?.getOrNull() ?: return null + return _hosterList.value.mapIndexed { index, h -> + if (hosterStateList[index] is HosterState.Ready) { + Hoster( + hosterName = h.hosterName, + hosterUrl = h.hosterUrl, + videoList = (hosterStateList[index] as HosterState.Ready).videoList, + ) + } else { + Hoster( + hosterName = h.hosterName, + hosterUrl = h.hosterUrl, + videoList = h.videoList, + ) + } + } + } +} @Composable fun EpisodeOptionsDialog( @@ -150,7 +395,15 @@ fun EpisodeOptionsDialog( episodeTitle: String, episode: Episode?, anime: Anime?, - resultList: Result>? = null, + showAllQualities: Boolean, + resultList: Result>? = null, + expandedList: List, + currentVideo: Video?, + selectedHosterVideoIndex: Pair, + onShowAllQualities: (Boolean) -> Unit, + onClickHoster: (Int) -> Unit, + onClickVideo: (Int, Int) -> Unit, + getHosterList: () -> List?, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -179,21 +432,36 @@ fun EpisodeOptionsDialog( style = MaterialTheme.typography.bodyMedium, ) - if (resultList == null || episode == null || anime == null) { + val onError: () -> Unit = { + logcat(LogPriority.ERROR) { "Error getting links" } + scope.launchUI { context.toast("No available videos") } + EpisodeOptionsDialogScreen.onDismissDialog() + } + if (resultList?.isFailure == true) { + onError() + } + + if (resultList == null || episode == null || anime == null || currentVideo == null) { LoadingScreen() } else { - val videoList = resultList.getOrNull() - if (!videoList.isNullOrEmpty()) { + val hosterStateList = resultList.getOrNull() + if (!hosterStateList.isNullOrEmpty()) { VideoList( useExternalDownloader = useExternalDownloader, episode = episode, anime = anime, - videoList = videoList, + showAllQualities = showAllQualities, + hosterStateList = hosterStateList, + expandedList = expandedList, + currentVideo = currentVideo, + selectedHosterVideoIndex = selectedHosterVideoIndex, + onShowAllQualities = onShowAllQualities, + onClickHoster = onClickHoster, + onClickVideo = onClickVideo, + getHosterList = getHosterList, ) } else { - logcat(LogPriority.ERROR) { "Error getting links" } - scope.launchUI { context.toast("Video list is empty") } - EpisodeOptionsDialogScreen.onDismissDialog() + onError() } } } @@ -204,7 +472,15 @@ private fun VideoList( useExternalDownloader: Boolean, episode: Episode, anime: Anime, - videoList: List