From 0ed5a62217fd465f51a2a83f36be81419c930a17 Mon Sep 17 00:00:00 2001 From: Dark25 Date: Wed, 6 Nov 2024 21:07:07 +0100 Subject: [PATCH] Upstream (#166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-alpha09 (#1039) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit ca784cbe3267e94e652e4c54f91b7107cc53c307) * Remove obsolete workaround Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com> * chore(deps): update softprops/action-gh-release action to v2.0.8 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency androidx.activity:activity-compose to v1.9.1 (#1042) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit e48dbdbf2356c0e6e148313dc6610e865cd8e995) * fix(deps): update dependency androidx.annotation:annotation to v1.8.1 (#1043) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 602b58f364b95b83a3148be34cd4c90d95d7d405) * fix(deps): update lifecycle.version to v2.8.4 (#1045) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit b7849d714698900a25188bdbfd77bf24936f2dd7) * Format Category String on Subtitle Display * Fixes #1029 * Max Line Length Fix * Update SettingsLibraryScreen.kt No idea how this works. Co-authored-by: Foolbar <118464521+Foolbar@users.noreply.github.com> --------- Co-authored-by: Foolbar <118464521+Foolbar@users.noreply.github.com> Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * fix(deps): update paging.version to v3.3.1 (#1046) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 41e2dc7ae80250d9166fc637c1170667afdb0a9e) * fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2024.07.00-alpha02 (#1051) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 04aa5b36a5893ef9991312d61f84f830b83535f7) * Fix disappearance items when fast scrolling * Don't use animateItem's fade-in/fade-out in FastScrollLazyColumn * Move to extension function Avoid using animateItemPlacement name since it's shadowed by compose-bom's deprecated one Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> * Adds Option to Copy Panel to Clipboard * Add Copy to Clipboard * Removing Unused Import * Reusing onShare function * Commit Suggestion * Early Return on null Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Fix library is backed up when disabled and make categories backup/restore independent Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Cleanup backup/restore related code Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Bump default user agent string (cherry picked from commit 8160b47ff5fbbd9b32caeb462b5be881fabd3449) * Improve error message if restoring from JSON file * Improve error message if restoring from JSON file * Replace Exception with IOException * Use more generic error message if protobuf fails * fix lint Co-authored-by: Vetle Ledaal <13540478+vetleledaal@users.noreply.github.com> * Match extra layout space with scroll distance (#1076) And increase recycler item view cache size. (cherry picked from commit a3dfd2efe6ace7a2a4d79bd09fb1a729989f1094) * chore(deps): update actions/setup-java action to v4.2.2 (#1080) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 3f6bd5f010532d292310c0c0f14bf11832277f2e) * fix: drawScrollbar crash on list with 0 item but only sticky header Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> * Don't crash on ill-formed URLs (#1084) (cherry picked from commit 854474f85ffc41eccdc2b3a6cf105fa2805ebc3c) * chore(deps): update kotlin monorepo to v2.0.10 (#1085) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit edb8201f74e516c296b62e04a13802e1bd9e0b6b) * Rename backup restore error log file Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add Backup and Restore of Extension Repos (#1057) * Backup/Restore Extension Repos * Refactor * Moving to Under App Settings * Sort by URL, Check existing by SHA and Error Logging Untested. Currently in a lecture and can't test if the changes really work. * Changes to logic * Don't ask me what's happening here * Renaming Variables * Fixing restoreAmount & changes to logic Co-Authored-By: AntsyLich <59261191+AntsyLich@users.noreply.github.com> --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * ExpandableMangaDescription: Adjust size transform anim spec Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * fix(deps): update paging.version to v3.3.2 (#1093) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 8e40146f96704c3dc98bbb4f9f89d470ffa32f69) * chore(deps): update gradle/actions action to v4 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency androidx.annotation:annotation to v1.8.2 (#1090) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 36b9caeea8baf15f0d0ed37abc12638d44194c09) * fix(deps): update dependency androidx.work:work-runtime to v2.9.1 (#1091) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit af77083660000e7378587dbc8d44e44bd8b196ec) * fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-alpha10 (#1092) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit e8b7c3e24bb677d289554b972ef2496a976c79aa) * fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2024.08.00-alpha01 (#1094) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit dca9bf105770890e015b8e2f9fbf22f05665e343) * fix(deps): update dependency com.android.tools.build:gradle to v8.5.2 (#1099) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 4828c54245dd6532c0e7a2b6c8cf5d8a703d3376) * Contributing: ktLintFormat -> detekt (#1102) * Contributing: ktLintFormat -> detekt update Contributing info to use detekt instead of ktLintFormat * Update CONTRIBUTING.md --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 14ae57d78b31f0bb3b58d19c1d8cfcebcc8e2253) * Change Kitsu to kitsu.app domain cf. hummingbird-me/kitsu-server@244fdcc Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Fix MAL search results not showing start dates The previous approach would always throw an Exception because `SimpleDateFormat.format()` expects the input to be of type `Date` or `Number`, not `String`. Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Translations update from Hosted Weblate (#939) Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ar/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ca/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/cs/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/de/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/es/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fil/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/id/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ja/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ml/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ru/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/be/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bg/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/da/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eu/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fa/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fi/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/gl/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/he/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/km/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/mr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nn/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sa/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sdh/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/te/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uk/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uz/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/ Translation: Mihon/Mihon Translation: Mihon/Mihon Plurals Co-authored-by: Ahmed seif al-nasr Co-authored-by: Ajeje Brazorf Co-authored-by: Akhil Raj Co-authored-by: Animeboynz <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: David Katrinka Co-authored-by: Dexroneum Co-authored-by: Eduard Ereza Martínez Co-authored-by: Eji-san Co-authored-by: FateXBlood Co-authored-by: Giorgio Sanna Co-authored-by: Iker Lerones Co-authored-by: Infy's Tagalog Translations Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com> Co-authored-by: Matyáš Caras Co-authored-by: Norsze Co-authored-by: Pitpe11 Co-authored-by: TheKingTermux Co-authored-by: abc0922001 Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: gallegonovato Co-authored-by: gekka <1778962971@qq.com> Co-authored-by: sebastians17 Co-authored-by: vodkapmp Co-authored-by: ɴᴇᴋᴏ Co-authored-by: Артём Голуб (cherry picked from commit b1b15a93eec15a82e2e83650abf97c1b9f0c501c) * Add Copy Tracker URL on icon long press * Add Copy Tracker URL on icon long press Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> * Add 'Copy To Clipboard' to tracker item menu Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> * Add 'Copy link' to locales. Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> * Implement code review suggestions > > Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> * Update app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt --------- Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add a button to select all scanlators Resolves #943 Closes #1109 (cherry picked from commit 84b2164787a795f3fd757c325cbfb6ef660ac3a3) * Fix UI freeze after migration Fixes #938 (cherry picked from commit 3f1d28c3833e6b868152149ed02b3fb8c54eccef) * Add an "open in browser" button to reader menu (#1110) * Add an "open in browser" button to reader menu Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> * fixup! Add an "open in browser" button to reader menu Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> --------- Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> (cherry picked from commit c5994e057b37484fec3a5300491946afe377a90a) * Handle Android SDK 35 API collision Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fix some migrations never running Both `SetupBackupCreateMigration` and `SetupLibraryUpdateMigration` were trying to get the `App` class from Injekt which is never provided via the `AppModule`. Using `Application` instead works since the `workManager` property used by the respective `setupTask` functions is an extension property on `Context`. Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Create CHANGELOG.md * Sync compose theme with MDC theme (cherry picked from commit 9a34ace09c66274e6c2b3f9446058a0fa99d4bd0) * Remove WebViewClientCompat (cherry picked from commit f4348df8709529b7b2319485fc8eb54c6e8173c7) * Add comment about RecyclerView cache size (#1119) Note for forks: Increasing cache size may cause OOM on API < 26, better to make it API 26+ only. (cherry picked from commit 1c47a6b9b35c622200c731cdbbc076f5263e8d06) * fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.0 (#1121) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 6f4e3f776f98d7a47dfa33b2cdfe992fc211ec28) * chore(deps): update dependency gradle to v8.10 (#1122) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 3f050a83dd0907e0ffb56a1e1833f9de5b10b329) * fix(deps): update dependency org.conscrypt:conscrypt-android to v2.5.3 (#1135) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit b2f1719c50365279e157a3b9ee015fc6c13a9a92) * Remove detekt (#1130) Annoying. More annoying in this project. (cherry picked from commit 777ae2461e1eb277a3aa0c998ff69e4f100387a1) * Remove more detekt annotations * Generate locales_config.xml in build dir Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add spotless (with ktlint) (#1136) (cherry picked from commit 5ae8095ef1ed2ae9f98486f9148e933c77a28692) * Address spotless lint errors * Translations update from Hosted Weblate Co-authored-by: Ahmed seif al-nasr Co-authored-by: Anas KANJO Co-authored-by: Dexroneum Co-authored-by: Frosted Co-authored-by: Hosted Weblate Co-authored-by: Infy's Tagalog Translations Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com> Co-authored-by: TheKingTermux Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: gallegonovato Co-authored-by: gekka <1778962971@qq.com> Co-authored-by: ɴᴇᴋᴏ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/tr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/ Translation: Mihon/Mihon Translation: Mihon/Mihon Plurals (cherry picked from commit 4387ae5ff3131dd4aaaacd75fa6e82e7b322d474) * fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.0 (#1142) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 08ae51ea8c5ceccc8c5c65120f387d7b19d18052) * fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2024.08.00-alpha02 (#1143) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 24817675320855cb01250acca87b97dd7ac8a399) * chore(deps): update kotlin monorepo to v2.0.20 (#1144) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 034ec4cb120c0f36cad1303de1314c28c4ec4969) * fix(deps): update moko to v0.24.2 (#1148) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 379d5878266ba0287bfcc4a06452c27d70f33ba1) * Fix lint errors * Add option to skip downloading duplicate read chapters * Add query to get chapter count by manga and chapter number * Add functions to get chapter count by manga and chapter number * Only count read chapters * Add interactor * Savepoint * Extract new chapter logic to separate function * Update javadocs * Add preference to toggle new functionality * Add todo * Add debug logcat * Use string resource instead of hardcoding title * Add temporary logcat for debugging * Fix detekt issues * Update javadocs * Update download unread chapters preference * Remove debug logcat calls * Update javadocs * Resolve issue where read chapters were still being downloaded during manual manga fetch * Apply code review changes * Apply code review changes * Revert "Apply code review changes" This reverts commit 1a2dce7. * Revert "Apply code review changes" This reverts commit ac2a778. * Group download chapter logic inside the interactor GetChaptersToDownload * Update javadocs * Apply code review * Apply code review * Apply code review * Update CHANGELOG.md to include the new feature * Run spotless * Update domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com> * fix(deps): update aboutlib.version to v11.2.3 (#1151) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit fba9bacdc19dee7cdf9e3d1cb4ee4a496fa7b514) * Respect privacy settings in extension update notification * Hide Extension Names in Update Notifications when Content is Hidden * Moving `val` inside if * [skip ci] Update CHANGELOG.md Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Add confirmation when adding repo via URI * Add confirmation when adding repo via URI * Blank lines * Suggestions * Reverting Changes * Removing Unused Imports Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Add "show entry" action to download notifications (#1159) * Add 'show entry' to download notifications Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> * fixup! Add 'show entry' to download notifications Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> * fixup! Add 'show entry' to download notifications Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> * spotless! Add 'show entry' to download notifications Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * fixup! spotless- Apply suggestions from code review Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> --------- Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.0 (#1162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 607e56a4ec6393a3bfd25fe74cbae676fd94df22) * chore(deps): update gradle/actions action to v4.0.1 (#1165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit d26c010e57ac0ed802cc811a029864972adfbb71) * Hide keyboard when a Tracker SearchResultItem is clicked * Hide keyboard on select * Code Review Suggestion Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Remove legacy broken source and history backup Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * fix(deps): update serialization.version to v1.7.2 (#1173) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 1837faa573f11a6b97fe13f358d6fa0e980c2ef7) * fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.1 (#1172) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 7fb3ef48e4fafce471173111fe1632754e5e9e99) * fix(deps): update dependency com.android.tools.build:gradle to v8.6.0 (#1178) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit f74071ab0a70c4fd649b451e58841539d011496a) * Use feature flags in compose compiler plugin And slight cleanup Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * PagerPageHolder: lazy init loading indicator Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Collect MangaScreen state with lifecycle Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add stable marker to Manga data class Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * `spotlessApply` my beloved * Fix import issue caused by version bump * Use DTOs to parse tracking API responses * Migrate tracking APIs to DTOs Changes the handling of tracker API responses to be parsed to DTOs instead of doing so "manually" by use of `jsonPrimitive`s and/or `Json.decodeFromString` invocations. This greatly simplifies the API response handling. Renamed constants to SCREAMING_SNAKE_CASE. Largely tried to name the DTOs in a uniform pattern, with the tracker's (short) name at the beginning of file and data class names (ALOAuth instead of OAuth, etc). With these changes, no area of the code base should be using `jsonPrimitive` and/or `Json.decodeFromString` anymore. * Fix wrong types in KitsuAlgoliaSearchItem This API returns start and end dates as Long and the score as Double. Kitsu's docs claim they're strings (and they are, when requesting manga details from Kitsu directly) but the Algolia search results return Longs and Double, respectively. * Apply review changes - Renamed `BangumiX` classes to `BGMX` classes. - Renamed `toXStatus` and `toXScore` to `toApiStatus` and `toApiScore` * Handle migration from detekt to spotless Removed Suppressions added for detekt. Specifically removed: - `SwallowedException` where an exception ends as a default value - `MagicNumber` - `CyclomaticComplexMethod` - `TooGenericExceptionThrown` Also ran spotlessApply which changed SMAddMangaResponse * Fix Kitsu failing to add series The `included` attribute seems to only appear when the user already has the entry in their Kitsu list. Since both `data` and `included` are required for `firstToTrack`, a guard clause has been added before all its calls. * Fix empty Bangumi error when entry doesn't exist Previously, the non-null assertion (!!) would cause a NullPointerException and a Toast with "Bangumi error: " (no message) when the user had removed their list entry from Bangumi through other means like the website. Now it will show "Bangumi error: Could not find manga". This is analogous to the error shown by Kitsu under these circumstances. * Fix Shikimori ignoring missing remote entry The user would see no indication that Shikimori could not properly refresh the track from the remote. This change causes the error Toast notification to pop up with the following message "Shikimori error: Could not find manga". This is analogous to Kitsu and Bangumi. * Remove usage of let where not needed These particular occurrences weren't needed because properties are directly accessible to further act upon. This neatly simplifies these clauses. * Remove missed let Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Enable 'Split Tall Images' by default (#1185) (cherry picked from commit 9c1905ede750f0229fad1a01431058b1cc9fb32d) * Option to update trackers when chapter marked as read * Track when marked as read * Add dismiss to snack bar * i18n & ignore decimal chapters * Detekt would have caught that 🤣 * `Ok` > `Yes` * Dont prompt if untracked or current > new * Move to MangaScreenModel * Suggestions Co-Authored-By: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Review 2 * toggleAllSelections first --------- Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Migrate some classpaths to gradle plugins Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.2 (#1188) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit c4d2fffb12c83c76cf48a85cbc9d7d754a4da39c) * Fix Kitsu `ratingTwenty` being typed as String The API docs and the responses type `ratingTwenty` as a "number" (Int in Kotlin, it's divided by 2 for a .5 step scale 0-10). It's nullable because an entry without a user rating returns `null` in that field. Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Rename LocalesConfigPlugin file to LocalesConfigTask Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Move archive related code to :core:archive Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Use new library for injekt with inorichi patch (cherry picked from commit c929854ae98b874bf1a7aceb82a15fbe3fb6a41f) * Fix moving of `openFileDescriptor` * Switch to stable compose (cherry picked from commit 2baffa62cade1abd978d5fd03151b47fc87fd31e) * fix(deps): update lifecycle.version to v2.8.5 (#1190) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 0e956cbb518e0e0827c1e7dfde8427cb8660a9fb) * fix(deps): update dependency com.google.accompanist:accompanist-systemuicontroller to v0.36.0 (#1192) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 29a74509a4af475694551808e317df96ea1146ad) * fix(deps): update dependency androidx.activity:activity-compose to v1.9.2 (#1189) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 52036e5664cbcf552de706adee6e0b4b972fe1c3) * Ignore "intent://" urls on webview ignore intent urls Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com> * `spotlessApply` my beloved * Use TextFieldState in BasicTextField where applicable Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Reduce ChapterNavigator horizontal padding on small ui Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Bump NDK version (#1203) (cherry picked from commit fbcc48fefc7ed050f6416a8684816730bcb5f8a8) * Show toast for app restart when User-Agent is changed (#1204) (cherry picked from commit c8ad6cdf31a14bce9a525cfc2a0616e8ac51d7c3) * fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.3.8 (#1198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 844dae1a4d23b88318e0ea482b38df4e3f5f2be2) * chore(deps): update dependency gradle to v8.10.1 (#1211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit fcb01b5bcf81e7c25ff820e99fcf10e867c3782f) * chore(deps): update actions/setup-java action to v4.3.0 (#1212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit be5d467955b386a5bab0c27347b4c183cd076e16) * fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.9.0 (#1222) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 1ff88dd9274db681ae0d76b39223389a1f758973) * chore(deps): update gradle/actions action to v4.1.0 (#1219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 86dd809f4d1bce450ac54da61afd034b5a43c757) * fix(deps): update dependency com.squareup.okio:okio to v3.9.1 (#1217) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit bebf80dfaec037559af061950083289a0ae23b44) * fix(deps): update dependency androidx.compose:compose-bom to v2024.09.01 (#1214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit d42f776c5c5ddd8fade02bc7d0117a7c3e1054d5) * Fix: wrong calculation of nextUpdate when setting custom fetchInterval Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> * Fix WheelPicker Manual Input (#1209) * Fix WheelPicker Manual Input * Lambda * inline * Update WheelPicker.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 339dc33f5833b224c01577da3da081deecdbbca2) * Fix Kitsu synopsis nullability This time, the Kitsu API docs are silent on whether this field (or any other field) can be null/undefined/etc, but it can happen and caused an error during search and update. This change just ensures the attribute is nullable and is set to an empty String when it is null. Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Re-enable fetching chapters list for entries with licenced status Enable Licensed Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * fix(deps): update dependency me.zhanghai.android.libarchive:library to v1.1.1 (#1229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 1e570bc9654fb0382a8d5b37923c9700e49be696) * fix(deps): update dependency com.android.tools.build:gradle to v8.6.1 (#1235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 0042cb6582f05d2a139b059bef81dc979e9a8ad6) * Change casing for Extention Repos String (#1248) (cherry picked from commit 2276abbb2373b94535e99c2d72ce0f7f6a1d008a) * fix(deps): update serialization.version to v1.7.3 (#1246) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 6dd93d70cc5c7fa39157d069b41be5557256537e) * fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.1 (#1238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit b3867dd63c714333f58678f13b4cafc708cbd918) * fix(deps): update lifecycle.version to v2.8.6 (#1241) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 418ba3026546b4785907c001a05006b609b490a3) * fix(deps): update dependency androidx.profileinstaller:profileinstaller to v1.4.0 (#1242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 380787a31021d710a8a6619d4e0c1b01e3e47941) * chore(deps): update actions/setup-java action to v4.4.0 (#1259) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit dde942df4eced3889bd61cec28b6cf59fe1c0de7) * fix(deps): update dependency me.zhanghai.android.libarchive:library to v1.1.2 (#1255) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit d04eeface97d64d921e9df23ffeba49d3eca2994) * chore(deps): update actions/checkout action to v4.2.0 (#1266) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 369df527b243e0a39687e5b77d63c7eed3a3772a) * fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.1 (#1262) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 2dd02b73d6059cef372e5d605efdafa7f60b47b0) * fix(deps): update dependency androidx.compose:compose-bom to v2024.09.02 (#1239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit 112b68b782d0f0ac027bf3d73ad28a8df0dc75b8) * chore(deps): update dependency gradle to v8.10.2 (#1254) * chore(deps): update dependency gradle to v8.10.2 * Update binaries --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit f7c8f1801ea8c7af7542ab8e3dce035ada495c7c) * fix(deps): update dependency com.android.tools.build:gradle to v8.7.0 (#1284) (cherry picked from commit cca33481dd1466ae6a9919796229586fe0937523) * fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.2 (#1287) (cherry picked from commit 6984e0465babed7638481b1982de7415612f32e5) * fix(deps): update dependency androidx.profileinstaller:profileinstaller to v1.4.1 (#1289) (cherry picked from commit c72c07f355a93f67d16166715dfdab88f2cc9201) * fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.2 (#1294) (cherry picked from commit 85ee9c6686ee4f4ca5519297df7c4b5482cc26c2) * fix(deps): update dependency androidx.compose:compose-bom to v2024.09.03 (#1288) (cherry picked from commit f7fbc93833c6107791680412cc110336d0e4e717) * Fix AniList `ALSearchItem.status` nullibility * chore(deps): update actions/checkout action to v4.2.1 (#1304) (cherry picked from commit 6adfa4fd0fdd320aedaeaf2d6cccf798e46dd6c4) * fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-rc01 (#1308) (cherry picked from commit 8113b77f1e762629f31cbcc5b9163819c6384a8b) * Update renovate configuration - Remove package rule for "dev.chrisbanes.compose:compose-bom" - Disable semantic commits (cherry picked from commit aa998071a1f476a6078f19500bc58f7855c3f8ae) * Update dependency io.mockk:mockk to v1.13.13 (#1313) (cherry picked from commit a2dc88965b8b06cd40d65b75450e1ca4a1e08bd4) * Retain remote last chapter read if it's higher than the local one for EnhancedTracker Co-authored-by: brewkunz <102181083+brewkunz@users.noreply.github.com> * Update kotlin monorepo to v2.0.21 (#1314) (cherry picked from commit 016f627fb0998dabcd6aea907b54365aa4e6a285) * Cleanup `LibraryScreenModel` `LibraryMap.applySort` and some more Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * run `spotlessApply` * Tweak Preference.collectAsState Co-authored-by: p (cherry picked from commit 3bddb5538528c19388e364d21e6a6c16487af759) * Adjust distinct checker in WidgetManager and run on default dispatcher Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Update resources exclusion rules Co-authored-by: p (cherry picked from commit 481cfedf08576cecfbb35616837bd8f627d8f959) * Bump compile sdk to 35 Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * ChapterNavigator: dispatch page change only when needed Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Remove usage of deprecated accompanist SystemUiController Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * run `spotlessApply` * Tweak profile compilation status output Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add Quantity Badge to Upcoming Screen Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Added random library sort Co-authored-by: Jack Hamilton <4615800+jackhamilton@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Update dependency me.zhanghai.android.libarchive:library to v1.1.3 (#1321) (cherry picked from commit 0a4ad89b9902061e3e2c2d9f2eb71f6b33c5c01c) * Confirmation dialog when removing privately installed extensions Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fix EnhancedTracker not auto binding when adding manga to library Co-authored-by: brewkunz <102181083+brewkunz@users.noreply.github.com> * Run PR check when base strings are changed Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Refrain from running spotless on weblate files Those are akin to generated files and are likely to not follow our formatting (cherry picked from commit 32d2c2ac1bc224cbda2f09a4023d7d120ea0e954) * Adjust expandable fab animation Co-authored-by: p (cherry picked from commit eb6092bd0cfa09694985a8bafdd8bbf2815190a1) * Add option to backup non-library read entries Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: jobobby04 Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fix PR build check (cherry picked from commit 9503082d44b5bd868ee1bfc42741dc978d1d9047) * Cleanup .gitignore files (cherry picked from commit afa50029882655af8d5eea40aed7644fce4564d8) * Reorder reader menu overflow items Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Make sure random library sort is at the bottom Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Update dependency com.android.tools.build:gradle to v8.7.1 (#1326) (cherry picked from commit 48166b9b52836f225273651b21fb02e7aba4197e) * Update i18n readme Co-authored-by: FlaminSarge <2764675+FlaminSarge@users.noreply.github.com> * Update dependency androidx.activity:activity-compose to v1.9.3 (#1333) (cherry picked from commit ba1343bed8c00d5ed976111c710c9b5648676a59) * Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.3 (#1334) (cherry picked from commit 572ee2f02a980a60a1120e7c0c88060fb1a7b3d2) * Update dependency androidx.glance:glance-appwidget to v1.1.1 (#1335) (cherry picked from commit 443f6e0ae53dadce1f66818fac0cd1eeaa5fec27) * Update dependency androidx.annotation:annotation to v1.9.0 (#1336) (cherry picked from commit 337806d9e17e92a9134d59324e9857d05abc4db3) * Change "Invalidate downloads index" to "Reindex downloads" Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Update xml.serialization.version to v0.90.2 (#1331) * Update xml.serialization.version to v0.90.2 * Fix build --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit dbf6ad2ca7e0525f597010709e87d094d10e4f8d) * Update dependency androidx.compose:compose-bom to v2024.10.00 (#1338) (cherry picked from commit 5612ae0149e9231c9691ee782da8159489a0d057) * Revert "Tweak Preference.collectAsState" This reverts commit 3bddb5538528c19388e364d21e6a6c16487af759. Fixes #1341 (cherry picked from commit eb3bea8150ce9bf2320d15c879cbebaa6d51a4c6) * Address deprecation, suggestion and spotless Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Pass uncaught exception to default handler in GlobalExceptionHandler Fixes #1347 (cherry picked from commit f3a2f566c8a09ab862758ae69b43da2a2cd8f1db) * Update dependency org.junit.jupiter:junit-jupiter to v5.11.3 (#1351) (cherry picked from commit e16c3953c709a6c35c4655f916119fdf665baa62) * Update shizuku.version to v13.1.0 (cherry picked from commit c550a81598c98ef9a22dac8f6a408f5c15235fde) * Update actions/dependency-review-action action to v4.3.5 (#1354) (cherry picked from commit e1e3ca7a565503d325322fbbdbff01868f6f2bcb) * Update actions/checkout action to v4.2.2 (#1361) (cherry picked from commit 01b44c0458eb77f8d5347328be0c3ef25c906b1b) * Make renovate group github action deps (cherry picked from commit d4bf19f957cf32671b7306076ac5bd5c94732d8b) * Pin actions/upload-artifact action to b4b15b8 (#1363) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 47b0e9d7bec5ab0c7d16a3c70999eaac8636f633) * Rework Auto Track on Mark as Read Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Fix settings SliderItem steps count (#1356) (cherry picked from commit 2ba7ed32802ffca1946d567b8afe49bfd3f4326e) * Avoid blocking call to load categories in settings Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> * Update dependency me.zhanghai.android.libarchive:library to v1.1.4 (#1378) (cherry picked from commit aae0e3459ce13398a64b5cd9995f4a40a0120822) * Cleanup Slider usage (cherry picked from commit df9fff60da3a38acd8fcd540b5fdd275be93f2d5) * Update actions/setup-java action to v4.5.0 (#1366) (cherry picked from commit 2bf7ef5d18f839e31c501f4e6e1abff9fa7f74d6) * Update dependency com.pinterest.ktlint:ktlint-cli to v1.4.0 Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Mend Renovate * Add libs.material to presentation-widget (#1373) Fixes some build issues (cherry picked from commit 264030d6ecbc7492d884eb328b74399cd722dcb0) * Allow completely disabling "Update tracker" snackbar on mark as read Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> * Fix app crash when removing tracked entry from tracker Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> * Release v0.17.0 (a.k.a. bump versionCode) Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Here lies "currentTab was used multiple times" Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fix sporadically recurring spotless CI failure Somehow this specific issue keeps getting flagged by unrelated PRs' CI runs (but only sometimes? Somehow? Other times the CI run would succeed with no spotless issues.) --------- Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Switch to spotless 7.0.0 Beta 4 (cherry picked from commit b8b053b1d720a6de5c3d4d8a683eed7bc8cdcc5f) * Update dependency androidx.viewpager:viewpager to v1.1.0-beta01 (#1414) (cherry picked from commit 9d6ddb5d91bd062876bdb108ca3ce278359551e5) * Update dependency androidx.annotation:annotation to v1.9.1 (#1413) (cherry picked from commit eedece5adfbb95c882d4d59a5020f7e27c634c13) * Some improvements to Bangumi tracker search Probably fix the anime side as well In short: - fetch & show actual summary - fallback to "name" if "name_cn" is empty - request larger responseGroup to get & display the summary & rating - add type filter query param to make Bangumi filter, not us Previously, we only displayed the "name" in the summary area and used "name_cn" as the entry name. However, "name_cn" (Chinese name) can be an empty string at times, resulting in an awkward looking search result list where some, many, or even all the results have no title displayed and only show the "name" (Japanese name) in the summary area. This has been solved by using "name" as a fallback value should "name_cn" be empty. If a Chinese name is available, the original name is prepended to the summary with the addition "作品原名:" (meaning "original series title"). By using the "responseGroup=large" query parameter, we can request the required data we need to display the actual summary for an entry and the entry's average rating. The "name" is prepended to the summary contents, if any exist, so it is still accessible for series identification if a "name_cn" exists too and was used for the result title. Adding the "type=1" filter query parameter means Bangumi will only return entries of type 1 ("book") instead of all types and Mihon needing to filter, resulting in potentially missed entry matches. Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> * Update lifecycle.version to v2.8.7 (#1415) (cherry picked from commit 328ec8c752f276a6e75f68102a257880e4b18753) * Update dependency androidx.constraintlayout:constraintlayout to v2.2.0 (#1416) (cherry picked from commit 2914d166fe0ad5d6bb126fd5fe89d8ca3074787b) * Update actions/dependency-review-action action to v4.4.0 (#1402) (cherry picked from commit 41ae8505fecd08c77bf316172e29698dd12b4023) * Update dependency io.coil-kt.coil3:coil-bom to v3.0.0-rc02 (#1401) (cherry picked from commit f33a6d25209fa9a1291f3dae222fc0ff8d95dba9) * Add to CHANGELOG.md * run `spotlessApply` * fix: Subtitle selection not matching two letter language codes (#1805) * chore(i18n): Translations update from Hosted Weblate (#1788) Co-authored-by: ᎽᎪՏՏᎬᎡ ᏴᎬΝ ᎻᎪᎷᎡΘႮᏟᎻᎬ Co-authored-by: 何意挽秋風 <94283631+RejectVanity@users.noreply.github.com> Co-authored-by: Renn Co-authored-by: Matt Co-authored-by: 翻訳する男 Co-authored-by: Reno Tx Co-authored-by: Mohammed al-Qubati Co-authored-by: Jonathan B Co-authored-by: Akhil Raj <89210430+akhi07rx@users.noreply.github.com> Co-authored-by: Frosted Co-authored-by: N. Hao Co-authored-by: Ajeje Brazorf Co-authored-by: jmir1 * chore(i18n): Weblate automatic tasks (#1811) * fix merge * fix spotless * te odio spotless --------- Signed-off-by: Catt0s <5874051+mm12@users.noreply.github.com> Signed-off-by: Catting <5874051+mm12@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: FooIbar <118464521+fooibar@users.noreply.github.com> Co-authored-by: Secozzi Co-authored-by: Foolbar <118464521+Foolbar@users.noreply.github.com> Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: Vetle Ledaal <13540478+vetleledaal@users.noreply.github.com> Co-authored-by: Tran M. Cuong Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: Catting <5874051+mm12@users.noreply.github.com> Co-authored-by: MajorTanya <39014446+majortanya@users.noreply.github.com> Co-authored-by: Weblate (bot) Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com> Co-authored-by: Smol Ame <155411819+Smol-Ame@users.noreply.github.com> Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com> Co-authored-by: NGB-Was-Taken <76197326+NGB-Was-Taken@users.noreply.github.com> Co-authored-by: Mend Renovate Co-authored-by: Mend Renovate Co-authored-by: brewkunz <102181083+brewkunz@users.noreply.github.com> Co-authored-by: Jack Hamilton <4615800+jackhamilton@users.noreply.github.com> Co-authored-by: jobobby04 Co-authored-by: FlaminSarge <2764675+FlaminSarge@users.noreply.github.com> Co-authored-by: abdurisaq <133296208+abdurisaq@users.noreply.github.com> Co-authored-by: jmir1 Co-authored-by: Secozzi <49240133+Secozzi@users.noreply.github.com> Co-authored-by: ᎽᎪՏՏᎬᎡ ᏴᎬΝ ᎻᎪᎷᎡΘႮᏟᎻᎬ Co-authored-by: 何意挽秋風 <94283631+RejectVanity@users.noreply.github.com> Co-authored-by: Renn Co-authored-by: Matt Co-authored-by: 翻訳する男 Co-authored-by: Reno Tx Co-authored-by: Mohammed al-Qubati Co-authored-by: Jonathan B Co-authored-by: Akhil Raj <89210430+akhi07rx@users.noreply.github.com> Co-authored-by: Frosted Co-authored-by: N. Hao Co-authored-by: Ajeje Brazorf --- .github/renovate.json5 | 11 +- .github/workflows/build_pull_request.yml | 39 +- .github/workflows/build_push.yml | 19 +- .gitignore | 23 +- CHANGELOG.md | 66 + CONTRIBUTING.md | 4 - README.md | 2 +- app/.gitignore | 3 - app/.idea/discord.xml | 2 +- app/.idea/gradle.xml | 2 +- app/.idea/misc.xml | 2 +- app/.idea/vcs.xml | 2 +- app/build.gradle.kts | 19 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- app/src/debug/res/mipmap/ic_launcher.xml | 2 +- .../debug/res/mipmap/ic_launcher_round.xml | 6 + app/src/main/java/aniyomi/util/DataSaver.kt | 3 +- .../eu/kanade/core/util/CollectionUtils.kt | 20 +- .../java/eu/kanade/domain/DomainModule.kt | 4 + .../track/anime/interactor/AddAnimeTracks.kt | 10 +- .../SyncEpisodeProgressWithTrack.kt | 4 +- .../track/manga/interactor/AddMangaTracks.kt | 10 +- .../SyncChapterProgressWithTrack.kt | 4 +- .../domain/track/model/AutoTrackState.kt | 10 + .../domain/track/service/TrackPreferences.kt | 7 + .../java/eu/kanade/domain/ui/UiPreferences.kt | 6 +- .../eu/kanade/domain/ui/model/NavStyle.kt | 4 +- .../eu/kanade/domain/ui/model/StartScreen.kt | 2 +- .../anime/AnimeExtensionDetailsScreen.kt | 2 +- .../browse/anime/AnimeExtensionsScreen.kt | 11 +- .../browse/anime/AnimeSourcesFilterScreen.kt | 5 +- .../browse/anime/AnimeSourcesScreen.kt | 4 +- .../anime/components/BrowseAnimeIcons.kt | 2 +- .../manga/MangaExtensionDetailsScreen.kt | 2 +- .../browse/manga/MangaExtensionsScreen.kt | 11 +- .../browse/manga/MangaSourcesFilterScreen.kt | 5 +- .../browse/manga/MangaSourcesScreen.kt | 4 +- .../manga/components/BrowseMangaIcons.kt | 2 +- .../presentation/components/AdaptiveSheet.kt | 9 - .../presentation/components/TabbedDialog.kt | 2 - .../presentation/components/TabbedScreen.kt | 11 +- .../presentation/entries/anime/AnimeScreen.kt | 21 +- .../anime/EpisodeOptionsDialogScreen.kt | 30 +- .../anime/components/AnimeCoverDialog.kt | 4 +- .../anime/components/AnimeEpisodeListItem.kt | 14 +- .../anime/components/AnimeInfoHeader.kt | 92 +- .../components/EpisodeDownloadIndicator.kt | 3 +- .../entries/components/ItemHeader.kt | 4 +- .../presentation/entries/manga/MangaScreen.kt | 12 +- .../manga/components/MangaChapterListItem.kt | 10 +- .../manga/components/MangaCoverDialog.kt | 4 +- .../manga/components/MangaInfoHeader.kt | 92 +- .../manga/components/ScanlatorFilterDialog.kt | 10 +- .../history/anime/AnimeHistoryScreen.kt | 5 +- .../history/manga/MangaHistoryScreen.kt | 5 +- .../anime/AnimeLibrarySettingsDialog.kt | 49 +- .../library/components/CommonEntryItem.kt | 8 +- .../manga/MangaLibrarySettingsDialog.kt | 47 +- .../presentation/more/NewUpdateScreen.kt | 2 +- .../presentation/more/settings/Preference.kt | 2 +- .../settings/screen/SettingsAdvancedScreen.kt | 3 +- .../settings/screen/SettingsDataScreen.kt | 3 +- .../settings/screen/SettingsDownloadScreen.kt | 23 +- .../settings/screen/SettingsLibraryScreen.kt | 23 +- .../settings/screen/SettingsPlayerScreen.kt | 54 +- .../settings/screen/SettingsSearchScreen.kt | 32 +- .../settings/screen/SettingsTrackingScreen.kt | 10 + .../screen/about/OpenSourceLicensesScreen.kt | 2 +- .../browse/AnimeExtensionReposScreen.kt | 11 +- .../browse/MangaExtensionReposScreen.kt | 11 +- .../browse/MangaExtensionReposScreenModel.kt | 1 + .../components/ExtensionReposDialogs.kt | 32 + .../screen/data/CreateBackupScreen.kt | 5 +- .../screen/data/RestoreBackupScreen.kt | 2 +- .../screen/data/SyncSettingsSelector.kt | 2 +- .../screen/debug/BackupSchemaScreen.kt | 6 +- .../settings/screen/debug/DebugInfoScreen.kt | 7 +- .../settings/screen/debug/WorkerInfoScreen.kt | 6 +- .../settings/widget/TriStateListDialog.kt | 4 +- .../more/stats/components/StatsItem.kt | 4 +- .../presentation/reader/ChapterTransition.kt | 6 +- .../reader/ReaderPageActionsDialog.kt | 29 +- .../reader/appbars/ReaderAppBars.kt | 14 +- .../reader/components/ChapterNavigator.kt | 50 +- .../track/anime/AnimeTrackInfoDialogHome.kt | 15 +- ...AnimeTrackInfoDialogHomePreviewProvider.kt | 2 + .../track/anime/AnimeTrackerSearch.kt | 31 +- .../AnimeTrackerSearchPreviewProvider.kt | 11 +- .../track/components/TrackLogoIcon.kt | 4 +- .../track/manga/MangaTrackInfoDialogHome.kt | 26 +- ...MangaTrackInfoDialogHomePreviewProvider.kt | 2 + .../track/manga/MangaTrackerSearch.kt | 31 +- .../MangaTrackerSearchPreviewProvider.kt | 11 +- .../updates/anime/AnimeUpdatesUiItem.kt | 15 +- .../updates/manga/MangaUpdatesUiItem.kt | 13 +- .../presentation/util/ExceptionFormatter.kt | 4 - .../util/FastScrollAnimateItem.kt | 8 + .../eu/kanade/presentation/util/Navigator.kt | 14 +- .../webview/WebViewScreenContent.kt | 11 + app/src/main/java/eu/kanade/tachiyomi/App.kt | 1 - .../tachiyomi/crash/GlobalExceptionHandler.kt | 11 +- .../tachiyomi/data/backup/BackupDecoder.kt | 27 +- .../data/backup/create/BackupCreator.kt | 116 +- .../data/backup/create/BackupOptions.kt | 38 +- .../create/creators/AnimeBackupCreator.kt | 2 +- ...tor.kt => AnimeCategoriesBackupCreator.kt} | 12 +- .../AnimeExtensionRepoBackupCreator.kt | 17 + .../creators/AnimeSourcesBackupCreator.kt | 29 + .../creators/ExtensionsBackupCreator.kt | 2 +- .../create/creators/MangaBackupCreator.kt | 2 +- .../creators/MangaCategoriesBackupCreator.kt | 19 + .../MangaExtensionRepoBackupCreator.kt | 17 + .../creators/MangaSourcesBackupCreator.kt | 29 + .../creators/PreferenceBackupCreator.kt | 4 +- .../create/creators/SourcesBackupCreator.kt | 50 - .../data/backup/full/models/Backup.kt | 6 +- .../tachiyomi/data/backup/models/Backup.kt | 6 +- .../data/backup/models/BackupAnime.kt | 4 +- .../data/backup/models/BackupAnimeHistory.kt | 11 - .../data/backup/models/BackupAnimeTracking.kt | 3 +- .../data/backup/models/BackupChapter.kt | 1 - .../data/backup/models/BackupEpisode.kt | 1 - .../backup/models/BackupExtensionRepos.kt | 24 + .../data/backup/models/BackupHistory.kt | 12 - .../data/backup/models/BackupManga.kt | 4 +- .../data/backup/restore/BackupRestorer.kt | 93 +- .../data/backup/restore/RestoreOptions.kt | 35 +- ...Restorer.kt => AnimeCategoriesRestorer.kt} | 36 +- .../restorers/AnimeExtensionRepoRestorer.kt | 38 + .../backup/restore/restorers/AnimeRestorer.kt | 4 +- .../restorers/MangaCategoriesRestorer.kt | 42 + .../restorers/MangaExtensionRepoRestorer.kt | 38 + .../backup/restore/restorers/MangaRestorer.kt | 4 +- .../restore/restorers/PreferenceRestorer.kt | 4 +- .../tachiyomi/data/coil/AnimeCoverFetcher.kt | 5 +- .../tachiyomi/data/coil/MangaCoverFetcher.kt | 5 +- .../connections/discord/DiscordRPCModels.kt | 48 +- .../connections/discord/DiscordRPCService.kt | 23 +- .../connections/discord/DiscordWebsocket.kt | 4 +- ...pcExternalAsset.kt => RPCExternalAsset.kt} | 4 +- .../data/database/models/anime/AnimeTrack.kt | 2 + .../database/models/anime/AnimeTrackImpl.kt | 2 + .../data/database/models/anime/Episode.kt | 2 + .../data/database/models/anime/EpisodeImpl.kt | 2 + .../data/database/models/manga/Chapter.kt | 2 + .../data/database/models/manga/ChapterImpl.kt | 2 + .../data/database/models/manga/MangaTrack.kt | 2 + .../database/models/manga/MangaTrackImpl.kt | 2 + .../data/download/anime/AnimeDownloadCache.kt | 76 +- .../download/anime/AnimeDownloadNotifier.kt | 26 +- .../download/anime/AnimeDownloadProvider.kt | 7 +- .../data/download/anime/AnimeDownloader.kt | 19 +- .../download/anime/model/AnimeDownload.kt | 18 +- .../data/download/manga/MangaDownloadCache.kt | 73 +- .../download/manga/MangaDownloadNotifier.kt | 25 +- .../data/download/manga/MangaDownloader.kt | 22 +- .../library/anime/AnimeLibraryUpdateJob.kt | 27 +- .../library/manga/MangaLibraryUpdateJob.kt | 17 +- .../data/notification/NotificationReceiver.kt | 40 + .../kanade/tachiyomi/data/sync/SyncManager.kt | 12 +- .../tachiyomi/data/track/MangaTracker.kt | 3 +- .../tachiyomi/data/track/TrackStatus.kt | 10 +- .../tachiyomi/data/track/anilist/Anilist.kt | 57 +- .../data/track/anilist/AnilistApi.kt | 224 +- .../data/track/anilist/AnilistInterceptor.kt | 10 +- .../data/track/anilist/AnilistModels.kt | 231 -- .../data/track/anilist/AnilistUtils.kt | 59 + .../data/track/anilist/dto/ALAddEntry.kt | 20 + .../data/track/anilist/dto/ALAnime.kt | 74 + .../data/track/anilist/dto/ALFuzzyDate.kt | 21 + .../data/track/anilist/dto/ALManga.kt | 74 + .../data/track/anilist/dto/ALOAuth.kt | 17 + .../data/track/anilist/dto/ALSearch.kt | 20 + .../data/track/anilist/dto/ALSearchItem.kt | 51 + .../data/track/anilist/dto/ALUser.kt | 26 + .../data/track/anilist/dto/ALUserList.kt | 55 + .../tachiyomi/data/track/bangumi/Bangumi.kt | 17 +- .../data/track/bangumi/BangumiApi.kt | 247 +- .../data/track/bangumi/BangumiInterceptor.kt | 26 +- .../data/track/bangumi/BangumiModels.kt | 74 - .../data/track/bangumi/BangumiUtils.kt | 22 + .../bangumi/dto/BGMCollectionResponse.kt | 28 + .../data/track/bangumi/dto/BGMOAuth.kt | 23 + .../data/track/bangumi/dto/BGMSearch.kt | 65 + .../data/track/bangumi/dto/BGMUser.kt | 23 + .../data/track/jellyfin/JellyfinApi.kt | 10 +- .../track/jellyfin/JellyfinInterceptor.kt | 1 - .../{JellyfinModels.kt => dto/JFItem.kt} | 12 +- .../tachiyomi/data/track/kitsu/Kitsu.kt | 7 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 290 +- .../data/track/kitsu/KitsuDateHelper.kt | 4 +- .../data/track/kitsu/KitsuInterceptor.kt | 10 +- .../tachiyomi/data/track/kitsu/KitsuModels.kt | 216 -- .../tachiyomi/data/track/kitsu/KitsuUtils.kt | 30 + .../data/track/kitsu/dto/KitsuAddEntry.kt | 13 + .../data/track/kitsu/dto/KitsuListSearch.kt | 115 + .../data/track/kitsu/dto/KitsuOAuth.kt | 20 + .../data/track/kitsu/dto/KitsuSearch.kt | 75 + .../track/kitsu/dto/KitsuSearchItemCover.kt | 8 + .../data/track/kitsu/dto/KitsuUser.kt | 13 + .../data/track/mangaupdates/MangaUpdates.kt | 6 +- .../track/mangaupdates/MangaUpdatesApi.kt | 90 +- .../dto/{Context.kt => MUContext.kt} | 2 +- .../mangaupdates/dto/{Image.kt => MUImage.kt} | 4 +- .../dto/{ListItem.kt => MUListItem.kt} | 8 +- .../track/mangaupdates/dto/MULoginResponse.kt | 8 + .../dto/{Rating.kt => MURating.kt} | 4 +- .../dto/{Record.kt => MURecord.kt} | 6 +- .../data/track/mangaupdates/dto/MUSearch.kt | 13 + .../dto/{Series.kt => MUSeries.kt} | 2 +- .../dto/{Status.kt => MUStatus.kt} | 2 +- .../mangaupdates/dto/{Url.kt => MUUrl.kt} | 2 +- .../data/track/model/AnimeTrackSearch.kt | 2 + .../data/track/model/MangaTrackSearch.kt | 2 + .../data/track/myanimelist/MyAnimeList.kt | 9 +- .../data/track/myanimelist/MyAnimeListApi.kt | 240 +- .../myanimelist/MyAnimeListInterceptor.kt | 11 +- ...AnimeListModels.kt => MyAnimeListUtils.kt} | 14 - .../data/track/myanimelist/dto/MALAnime.kt | 26 + .../data/track/myanimelist/dto/MALList.kt | 48 + .../data/track/myanimelist/dto/MALManga.kt | 26 + .../data/track/myanimelist/dto/MALOAuth.kt | 23 + .../data/track/myanimelist/dto/MALSearch.kt | 18 + .../data/track/myanimelist/dto/MALUser.kt | 8 + .../myanimelist/dto/MALUserListSearch.kt | 25 + .../data/track/shikimori/Shikimori.kt | 13 +- .../data/track/shikimori/ShikimoriApi.kt | 197 +- .../track/shikimori/ShikimoriInterceptor.kt | 12 +- .../{ShikimoriModels.kt => ShikimoriUtils.kt} | 13 - .../track/shikimori/dto/SMAddEntryResponse.kt | 8 + .../data/track/shikimori/dto/SMEntry.kt | 57 + .../data/track/shikimori/dto/SMOAuth.kt | 21 + .../data/track/shikimori/dto/SMUser.kt | 8 + .../track/shikimori/dto/SMUserListEntry.kt | 42 + .../tachiyomi/data/track/simkl/Simkl.kt | 9 +- .../tachiyomi/data/track/simkl/SimklApi.kt | 174 +- .../data/track/simkl/SimklInterceptor.kt | 11 +- .../simkl/{SimklModels.kt => SimklUtils.kt} | 8 - .../data/track/simkl/dto/SimklOAuth.kt | 10 + .../data/track/simkl/dto/SimklSearch.kt | 44 + .../data/track/simkl/dto/SimklSyncItem.kt | 73 + .../data/track/simkl/dto/SimklSyncWatched.kt | 12 + .../data/track/simkl/dto/SimklUser.kt | 13 + .../extension/ExtensionUpdateNotifier.kt | 16 +- .../kanade/tachiyomi/extension/InstallStep.kt | 8 +- .../extension/anime/AnimeExtensionManager.kt | 50 +- .../anime/installer/InstallerAnime.kt | 2 +- .../anime/util/AnimeExtensionLoader.kt | 11 +- .../extension/manga/MangaExtensionManager.kt | 50 +- .../manga/installer/InstallerManga.kt | 2 +- .../manga/util/MangaExtensionLoader.kt | 11 +- .../base/delegate/SecureActivityDelegate.kt | 3 +- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 41 +- .../extension/AnimeExtensionsScreenModel.kt | 14 +- .../anime/extension/AnimeExtensionsTab.kt | 63 +- .../source/browse/BrowseAnimeSourceScreen.kt | 2 +- .../extension/MangaExtensionsScreenModel.kt | 14 +- .../manga/extension/MangaExtensionsTab.kt | 63 +- .../source/browse/BrowseMangaSourceScreen.kt | 5 +- .../tachiyomi/ui/category/CategoriesTab.kt | 32 +- .../tachiyomi/ui/download/DownloadsTab.kt | 4 +- .../tachiyomi/ui/entries/anime/AnimeScreen.kt | 12 +- .../ui/entries/anime/AnimeScreenModel.kt | 65 +- .../anime/track/AnimeTrackInfoDialog.kt | 24 +- .../tachiyomi/ui/entries/manga/MangaScreen.kt | 15 +- .../ui/entries/manga/MangaScreenModel.kt | 70 +- .../manga/track/MangaTrackInfoDialog.kt | 24 +- .../tachiyomi/ui/history/HistoriesTab.kt | 2 +- .../eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 19 +- .../library/anime/AnimeLibraryScreenModel.kt | 50 +- .../anime/AnimeLibrarySettingsScreenModel.kt | 2 +- .../ui/library/anime/AnimeLibraryTab.kt | 4 +- .../library/manga/MangaLibraryScreenModel.kt | 46 +- .../manga/MangaLibrarySettingsScreenModel.kt | 2 +- .../ui/library/manga/MangaLibraryTab.kt | 5 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 79 +- .../eu/kanade/tachiyomi/ui/more/MoreTab.kt | 16 +- .../ui/player/CastOptionsProvider.kt | 2 +- .../tachiyomi/ui/player/ExternalIntents.kt | 58 +- .../tachiyomi/ui/player/PlayerActivity.kt | 48 +- .../tachiyomi/ui/player/PlayerViewModel.kt | 27 +- .../settings/PlayerSettingsScreenModel.kt | 1 - .../settings/dialogs/EpisodeListDialog.kt | 6 +- .../player/settings/dialogs/PlayerDialog.kt | 10 - .../settings/dialogs/SkipIntroLengthDialog.kt | 7 +- .../settings/sheets/PlayerSettingsSheet.kt | 1 - .../settings/sheets/ScreenshotOptionsSheet.kt | 1 - .../settings/sheets/StreamsCatalogSheet.kt | 1 - .../settings/sheets/VideoChaptersSheet.kt | 1 - .../sheets/subtitle/SubtitleFontPage.kt | 6 +- .../sheets/subtitle/SubtitleSettingsSheet.kt | 1 - .../ui/player/viewer/PlayerControlsView.kt | 2 +- .../tachiyomi/ui/player/viewer/PlayerEnums.kt | 23 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 35 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 40 +- .../ui/reader/loader/ArchivePageLoader.kt | 2 +- .../ui/reader/loader/ChapterLoader.kt | 5 +- .../ui/reader/loader/DownloadPageLoader.kt | 2 +- .../ui/reader/loader/EpubPageLoader.kt | 21 +- .../ui/reader/loader/HttpPageLoader.kt | 4 +- .../ui/reader/setting/ReaderPreferences.kt | 2 +- .../ui/reader/viewer/ReaderPageImageView.kt | 4 +- .../ui/reader/viewer/ReaderTransitionView.kt | 19 +- .../ui/reader/viewer/pager/PagerPageHolder.kt | 46 +- .../ui/reader/viewer/pager/PagerViewer.kt | 5 +- .../reader/viewer/pager/PagerViewerAdapter.kt | 9 +- .../viewer/webtoon/WebtoonLayoutManager.kt | 8 +- .../viewer/webtoon/WebtoonRecyclerView.kt | 6 +- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 16 +- .../connections/DiscordLoginActivity.kt | 2 +- .../eu/kanade/tachiyomi/ui/stats/StatsTab.kt | 19 +- .../kanade/tachiyomi/ui/storage/StorageTab.kt | 18 +- .../kanade/tachiyomi/ui/updates/UpdatesTab.kt | 4 +- .../ui/webview/WebViewScreenModel.kt | 8 +- .../kanade/tachiyomi/util/AnimeExtensions.kt | 26 - .../kanade/tachiyomi/util/MangaExtensions.kt | 26 - .../kanade/tachiyomi/util/SubtitleSelect.kt | 6 +- .../util/system/NotificationExtensions.kt | 6 +- .../core/migration/MigrationJobFactory.kt | 11 +- .../mihon/core/migration/MigrationStrategy.kt | 4 +- .../migration/MigrationStrategyFactory.kt | 4 +- .../java/mihon/core/migration/Migrator.kt | 4 +- .../migrations/EnableAutoBackupMigration.kt | 1 - .../MigrateRotationViewerValuesMigration.kt | 1 - .../migrations/MigrateSortingModeMigration.kt | 1 - .../migrations/RelativeTimestampMigration.kt | 1 - .../RemoveOneTwoHourUpdateMigration.kt | 1 - .../migrations/RemoveQuickUpdateMigration.kt | 1 - .../migrations/RemoveReaderTapMigration.kt | 1 - .../ResetSortPreferenceRemovedMigration.kt | 1 - .../SetupAnimeLibraryUpdateMigration.kt | 4 +- .../migrations/SetupBackupCreateMigration.kt | 4 +- .../SetupMangaLibraryUpdateMigration.kt | 4 +- .../anime/UpcomingAnimeScreenContent.kt | 46 +- .../anime/UpcomingAnimeScreenModel.kt | 17 +- .../upcoming/anime/UpcomingAnimeUIModel.kt | 2 +- .../upcoming/components/calendar/Calendar.kt | 12 +- .../components/calendar/CalendarDay.kt | 9 +- .../components/calendar/CalendarHeader.kt | 10 +- .../components/calendar/CalendarIndicator.kt | 8 +- .../manga/UpcomingMangaScreenContent.kt | 46 +- .../manga/UpcomingMangaScreenModel.kt | 17 +- .../upcoming/manga/UpcomingMangaUIModel.kt | 2 +- app/src/main/res/anim/player_enter_bottom.xml | 2 +- app/src/main/res/anim/player_enter_left.xml | 2 +- app/src/main/res/anim/player_enter_right.xml | 2 +- app/src/main/res/anim/player_enter_top.xml | 2 +- app/src/main/res/anim/player_exit_bottom.xml | 2 +- app/src/main/res/anim/player_exit_left.xml | 2 +- app/src/main/res/anim/player_exit_right.xml | 2 +- app/src/main/res/anim/player_exit_top.xml | 2 +- app/src/main/res/anim/player_fade_in.xml | 2 +- app/src/main/res/anim/player_fade_out.xml | 2 +- .../ic_book_open_variant_24dp.xml | 2 +- .../ic_page_next_outline_24dp.xml | 2 +- app/src/main/res/drawable/ic_ani.xml | 2 +- .../drawable/ic_ani_monochrome_launcher.xml | 2 +- .../drawable/ic_animelibrary_filled_24dp.xml | 2 +- .../ic_animelibrary_selector_24dp.xml | 2 +- .../res/drawable/ic_banner_foreground.xml | 2 +- .../drawable/ic_brightness_negative_20dp.xml | 2 +- .../drawable/ic_brightness_positive_20dp.xml | 2 +- .../res/drawable/ic_browse_filled_24dp.xml | 2 +- app/src/main/res/drawable/ic_circle_200dp.xml | 2 +- .../res/drawable/ic_circle_right_200dp.xml | 2 +- app/src/main/res/drawable/ic_discord_24dp.xml | 2 +- app/src/main/res/drawable/ic_glasses_24dp.xml | 2 +- .../drawable/ic_pause_circle_filled_24.xml | 2 +- .../drawable/ic_picture_in_picture_20dp.xml | 2 +- .../res/drawable/ic_play_circle_filled_24.xml | 2 +- .../res/drawable/ic_play_seek_triangle.xml | 2 +- .../res/drawable/ic_progress_clock_24dp.xml | 2 +- app/src/main/res/drawable/ic_tag_24dp.xml | 2 +- app/src/main/res/drawable/ic_ungroup_24dp.xml | 2 +- .../res/drawable/ic_updates_outline_24dp.xml | 2 +- .../main/res/drawable/ic_volume_off_24dp.xml | 2 +- .../main/res/drawable/ic_volume_on_20dp.xml | 2 +- .../drawable/material_popup_background.xml | 2 +- app/src/main/res/drawable/player_bar.xml | 2 +- .../main/res/layout/dialog_stub_textinput.xml | 2 +- .../res/layout/discord_login_activity.xml | 2 +- .../main/res/layout/player_chapters_item.xml | 2 +- app/src/main/res/layout/player_controls.xml | 2 +- .../layout/player_double_tap_seek_view.xml | 2 +- .../main/res/layout/player_tracks_item.xml | 2 +- .../res/layout/pref_skip_intro_length.xml | 2 +- app/src/main/res/menu/expanded_controller.xml | 2 +- .../main/res/mipmap-anydpi-v26/ic_logo.xml | 2 +- app/src/main/res/mipmap/ic_banner.xml | 2 +- app/src/main/res/mipmap/ic_launcher.xml | 2 +- app/src/main/res/mipmap/ic_launcher_round.xml | 6 + app/src/main/res/raw/keep.xml | 2 +- .../res/values-night/colors_cottoncandy.xml | 1 - app/src/main/res/values-night/themes.xml | 9 +- .../main/res/values/colors_cottoncandy.xml | 3 +- .../main/res/values/ic_banner_background.xml | 2 +- app/src/main/res/values/themes.xml | 174 +- app/src/main/res/xml/s_pen_actions.xml | 2 +- .../java/mihon/core/migration/MigratorTest.kt | 4 +- build.gradle.kts | 6 +- buildSrc/.gitignore | 1 - buildSrc/build.gradle.kts | 2 +- ...hon.android.application.compose.gradle.kts | 2 + .../mihon.android.application.gradle.kts | 3 +- .../main/kotlin/mihon.benchmark.gradle.kts | 3 +- .../main/kotlin/mihon.code.detekt.gradle.kts | 47 - .../main/kotlin/mihon.code.lint.gradle.kts | 42 + .../kotlin/mihon.library.compose.gradle.kts | 3 +- .../src/main/kotlin/mihon.library.gradle.kts | 3 +- .../kotlin/mihon/buildlogic/AndroidConfig.kt | 4 +- .../mihon/buildlogic/ProjectExtensions.kt | 21 +- ...esConfigPlugin.kt => LocalesConfigTask.kt} | 8 +- config/detekt/baseline.xml | 2332 ----------------- config/detekt/detekt.yml | 22 - core-metadata/.gitignore | 1 - core-metadata/src/main/AndroidManifest.xml | 2 +- core/archive/build.gradle.kts | 15 + core/archive/src/main/AndroidManifest.xml | 2 + .../mihon/core}/archive/ArchiveEntry.kt | 2 +- .../mihon/core}/archive/ArchiveInputStream.kt | 4 +- .../mihon/core}/archive/ArchiveReader.kt | 18 +- .../kotlin/mihon/core/archive/EpubReader.kt} | 7 +- .../mihon/core/archive/UniFileExtensions.kt | 13 + .../kotlin/mihon/core}/archive/ZipWriter.kt | 11 +- core/common/.gitignore | 1 - core/common/src/main/AndroidManifest.xml | 2 +- .../tachiyomi/network/NetworkPreferences.kt | 2 +- .../eu/kanade/tachiyomi/network/Requests.kt | 2 + .../interceptor/CloudflareInterceptor.kt | 18 +- .../util/system/WebViewClientCompat.kt | 91 - .../tachiyomi/util/system/WebViewUtil.kt | 2 +- .../core/common/storage/UniFileExtensions.kt | 5 - .../core/common/util/system/ImageUtil.kt | 8 +- data/.gitignore | 1 - data/build.gradle.kts | 2 +- data/src/main/AndroidManifest.xml | 2 +- .../data/entries/anime/AnimeMapper.kt | 2 - .../data/entries/anime/AnimeRepositoryImpl.kt | 5 +- .../data/entries/manga/MangaMapper.kt | 2 - .../data/entries/manga/MangaRepositoryImpl.kt | 5 +- .../handlers/anime/QueryPagingAnimeSource.kt | 8 +- .../handlers/manga/QueryPagingMangaSource.kt | 8 +- .../items/chapter/ChapterRepositoryImpl.kt | 1 - .../items/episode/EpisodeRepositoryImpl.kt | 1 - data/src/main/sqldelight/data/mangas.sq | 9 + .../main/sqldelightanime/dataanime/animes.sq | 9 + domain/.gitignore | 1 - domain/build.gradle.kts | 2 + domain/src/main/AndroidManifest.xml | 2 +- .../interactor/CreateAnimeExtensionRepo.kt | 1 - .../interactor/CreateMangaExtensionRepo.kt | 1 - .../service/ExtensionRepoService.kt | 1 - .../interactor/FilterChaptersForDownload.kt | 72 + .../interactor/FilterEpisodesForDownload.kt | 72 + .../interactor/SetSortModeForAnimeCategory.kt | 4 + .../interactor/SetSortModeForMangaCategory.kt | 4 + .../download/service/DownloadPreferences.kt | 5 +- .../anime/interactor/AnimeFetchInterval.kt | 2 +- .../domain/entries/anime/model/Anime.kt | 2 + .../anime/repository/AnimeRepository.kt | 2 + .../manga/interactor/MangaFetchInterval.kt | 2 +- .../domain/entries/manga/model/Manga.kt | 2 + .../manga/repository/MangaRepository.kt | 2 + .../interactor/ShouldUpdateDbChapter.kt | 3 +- .../interactor/ShouldUpdateDbEpisode.kt | 3 +- .../anime/model/AnimeLibrarySortMode.kt | 4 + .../manga/model/MangaLibrarySortMode.kt | 4 + .../library/service/LibraryPreferences.kt | 5 + .../interactor/GetApplicationRelease.kt | 3 +- .../storage/service/StoragePreferences.kt | 1 + .../domain/library/model/LibraryFlagsTest.kt | 4 +- gradle/androidx.versions.toml | 20 +- gradle/compose.versions.toml | 13 +- gradle/kotlinx.versions.toml | 10 +- gradle/libs.versions.toml | 47 +- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +- gradlew.bat | 2 + i18n/.gitignore | 2 - i18n/README.md | 2 +- i18n/build.gradle.kts | 15 +- .../commonMain/moko-resources/am/plurals.xml | 2 +- .../commonMain/moko-resources/am/strings.xml | 2 +- .../commonMain/moko-resources/ar/plurals.xml | 2 +- .../commonMain/moko-resources/ar/strings.xml | 110 +- .../commonMain/moko-resources/as/strings.xml | 1 - .../moko-resources/base/plurals.xml | 2 +- .../moko-resources/base/strings.xml | 23 +- .../commonMain/moko-resources/be/plurals.xml | 2 +- .../commonMain/moko-resources/be/strings.xml | 2 +- .../commonMain/moko-resources/bg/plurals.xml | 2 +- .../commonMain/moko-resources/bg/strings.xml | 2 +- .../commonMain/moko-resources/bn/plurals.xml | 2 +- .../commonMain/moko-resources/bn/strings.xml | 2 +- .../commonMain/moko-resources/ca/plurals.xml | 7 +- .../commonMain/moko-resources/ca/strings.xml | 1 - .../commonMain/moko-resources/ceb/plurals.xml | 2 +- .../commonMain/moko-resources/ceb/strings.xml | 2 +- .../commonMain/moko-resources/cs/plurals.xml | 7 +- .../commonMain/moko-resources/cs/strings.xml | 1 - .../commonMain/moko-resources/cv/plurals.xml | 2 +- .../commonMain/moko-resources/cv/strings.xml | 2 +- .../commonMain/moko-resources/da/plurals.xml | 2 +- .../commonMain/moko-resources/da/strings.xml | 2 +- .../commonMain/moko-resources/de/plurals.xml | 6 +- .../commonMain/moko-resources/de/strings.xml | 1 - .../commonMain/moko-resources/el/plurals.xml | 2 +- .../commonMain/moko-resources/el/strings.xml | 1 - .../commonMain/moko-resources/eo/plurals.xml | 2 +- .../commonMain/moko-resources/eo/strings.xml | 2 +- .../commonMain/moko-resources/es/plurals.xml | 9 +- .../commonMain/moko-resources/es/strings.xml | 4 +- .../commonMain/moko-resources/eu/plurals.xml | 2 +- .../commonMain/moko-resources/eu/strings.xml | 2 +- .../commonMain/moko-resources/fa/plurals.xml | 2 +- .../commonMain/moko-resources/fa/strings.xml | 2 +- .../commonMain/moko-resources/fi/plurals.xml | 2 +- .../commonMain/moko-resources/fi/strings.xml | 2 +- .../commonMain/moko-resources/fil/plurals.xml | 2 +- .../commonMain/moko-resources/fil/strings.xml | 1 - .../commonMain/moko-resources/fr/plurals.xml | 2 +- .../commonMain/moko-resources/fr/strings.xml | 1 - .../commonMain/moko-resources/gl/plurals.xml | 2 +- .../commonMain/moko-resources/gl/strings.xml | 2 +- .../commonMain/moko-resources/he/plurals.xml | 2 +- .../commonMain/moko-resources/he/strings.xml | 1 - .../commonMain/moko-resources/hi/plurals.xml | 2 +- .../commonMain/moko-resources/hi/strings.xml | 2 +- .../commonMain/moko-resources/hr/plurals.xml | 2 +- .../commonMain/moko-resources/hr/strings.xml | 2 - .../commonMain/moko-resources/hu/plurals.xml | 2 +- .../commonMain/moko-resources/hu/strings.xml | 2 +- .../commonMain/moko-resources/in/plurals.xml | 5 +- .../commonMain/moko-resources/in/strings.xml | 16 +- .../commonMain/moko-resources/it/plurals.xml | 2 +- .../commonMain/moko-resources/it/strings.xml | 2 - .../moko-resources/ja/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/ja/plurals.xml | 5 +- .../commonMain/moko-resources/ja/strings.xml | 18 +- .../moko-resources/jv/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/jv/plurals.xml | 2 +- .../commonMain/moko-resources/jv/strings.xml | 2 +- .../moko-resources/ka-rGE/plurals.xml | 2 +- .../moko-resources/ka-rGE/strings.xml | 2 +- .../commonMain/moko-resources/kk/plurals.xml | 2 +- .../commonMain/moko-resources/kk/strings.xml | 2 +- .../moko-resources/km/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/km/plurals.xml | 2 +- .../commonMain/moko-resources/km/strings.xml | 2 +- .../commonMain/moko-resources/kn/plurals.xml | 2 +- .../commonMain/moko-resources/kn/strings.xml | 2 +- .../commonMain/moko-resources/ko/plurals.xml | 2 +- .../commonMain/moko-resources/ko/strings.xml | 1 - .../commonMain/moko-resources/lt/plurals.xml | 2 +- .../commonMain/moko-resources/lt/strings.xml | 2 +- .../commonMain/moko-resources/lv/plurals.xml | 2 +- .../commonMain/moko-resources/lv/strings.xml | 1 - .../commonMain/moko-resources/ml/plurals.xml | 12 +- .../commonMain/moko-resources/ml/strings.xml | 7 +- .../commonMain/moko-resources/mr/plurals.xml | 2 +- .../commonMain/moko-resources/mr/strings.xml | 2 +- .../moko-resources/ms/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/ms/plurals.xml | 2 +- .../commonMain/moko-resources/ms/strings.xml | 1 - .../moko-resources/nb-rNO/plurals.xml | 2 +- .../moko-resources/nb-rNO/strings.xml | 1 - .../commonMain/moko-resources/ne/plurals.xml | 2 +- .../commonMain/moko-resources/ne/strings.xml | 1 - .../commonMain/moko-resources/nl/plurals.xml | 2 +- .../commonMain/moko-resources/nl/strings.xml | 1 - .../commonMain/moko-resources/nn/plurals.xml | 2 +- .../commonMain/moko-resources/nn/strings.xml | 2 +- .../commonMain/moko-resources/pl/plurals.xml | 2 +- .../commonMain/moko-resources/pl/strings.xml | 2 +- .../moko-resources/pt-rBR/plurals.xml | 2 +- .../moko-resources/pt-rBR/strings.xml | 39 +- .../commonMain/moko-resources/pt/plurals.xml | 2 +- .../commonMain/moko-resources/pt/strings.xml | 32 +- .../commonMain/moko-resources/ro/plurals.xml | 2 +- .../commonMain/moko-resources/ro/strings.xml | 1 - .../commonMain/moko-resources/ru/plurals.xml | 8 +- .../commonMain/moko-resources/ru/strings.xml | 2 - .../commonMain/moko-resources/sa/plurals.xml | 2 +- .../commonMain/moko-resources/sa/strings.xml | 2 +- .../moko-resources/sah/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/sah/plurals.xml | 2 +- .../commonMain/moko-resources/sah/strings.xml | 2 +- .../commonMain/moko-resources/sc/plurals.xml | 2 +- .../commonMain/moko-resources/sc/strings.xml | 115 +- .../commonMain/moko-resources/sdh/plurals.xml | 2 +- .../commonMain/moko-resources/sdh/strings.xml | 2 +- .../commonMain/moko-resources/sk/plurals.xml | 2 +- .../commonMain/moko-resources/sk/strings.xml | 2 +- .../commonMain/moko-resources/sq/plurals.xml | 2 +- .../commonMain/moko-resources/sq/strings.xml | 2 +- .../commonMain/moko-resources/sr/plurals.xml | 2 +- .../commonMain/moko-resources/sr/strings.xml | 426 ++- .../commonMain/moko-resources/sv/plurals.xml | 2 +- .../commonMain/moko-resources/sv/strings.xml | 1 - .../commonMain/moko-resources/te/plurals.xml | 2 +- .../commonMain/moko-resources/te/strings.xml | 2 +- .../moko-resources/th/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/th/plurals.xml | 2 +- .../commonMain/moko-resources/th/strings.xml | 1 - .../commonMain/moko-resources/tr/plurals.xml | 12 +- .../commonMain/moko-resources/tr/strings.xml | 4 +- .../commonMain/moko-resources/uk/plurals.xml | 2 +- .../commonMain/moko-resources/uk/strings.xml | 1 - .../commonMain/moko-resources/uz/plurals.xml | 2 +- .../commonMain/moko-resources/uz/strings.xml | 2 +- .../moko-resources/vi/plurals-aniyomi.xml | 2 +- .../commonMain/moko-resources/vi/plurals.xml | 2 +- .../commonMain/moko-resources/vi/strings.xml | 5 +- .../moko-resources/zh-rCN/plurals.xml | 2 +- .../moko-resources/zh-rCN/strings.xml | 4 +- .../moko-resources/zh-rTW/plurals-aniyomi.xml | 2 +- .../moko-resources/zh-rTW/plurals.xml | 5 +- .../moko-resources/zh-rTW/strings.xml | 35 +- i18n/src/main/AndroidManifest.xml | 2 + macrobenchmark/.gitignore | 1 - presentation-core/.gitignore | 1 - .../src/main/AndroidManifest.xml | 2 +- .../core/components/AdaptiveSheet.kt | 4 +- .../core/components/SettingsItems.kt | 39 +- .../core/components/VerticalFastScroller.kt | 3 +- .../core/components/WheelPicker.kt | 56 +- .../core/components/material/Button.kt | 4 +- .../core/components/material/Constants.kt | 4 +- .../material/FloatingActionButton.kt | 34 +- .../core/components/material/Scaffold.kt | 5 +- .../core/components/material/Slider.kt | 48 + .../presentation/core/icons/Discord.kt | 17 +- .../presentation/core/icons/Github.kt | 17 +- .../presentation/core/util/Modifier.kt | 4 +- .../presentation/core/util/Scrollbar.kt | 6 +- .../res/values-night/color_cloudflare.xml | 2 - .../src/main/res/values-night/color_doom.xml | 1 - .../main/res/values-night/color_lavender.xml | 46 +- .../main/res/values-night/color_matrix.xml | 1 - .../main/res/values-night/color_sapphire.xml | 1 - .../src/main/res/values-night/colors.xml | 5 - .../res/values-night/colors_greenapple.xml | 53 +- .../res/values-night/colors_midnightdusk.xml | 21 +- .../src/main/res/values-night/colors_nord.xml | 20 +- .../res/values-night/colors_strawberry.xml | 49 +- .../res/values-night/colors_tachiyomi.xml | 49 +- .../src/main/res/values-night/colors_tako.xml | 21 +- .../res/values-night/colors_tealturqoise.xml | 16 +- .../res/values-night/colors_tidalwave.xml | 17 +- .../main/res/values-night/colors_yinyang.xml | 14 +- .../main/res/values-night/colors_yotsuba.xml | 14 +- .../src/main/res/values/color_cloudflare.xml | 1 - .../src/main/res/values/color_doom.xml | 1 - .../src/main/res/values/color_lavender.xml | 42 +- .../src/main/res/values/color_matrix.xml | 1 - .../src/main/res/values/color_sapphire.xml | 1 - .../src/main/res/values/colors.xml | 14 +- .../src/main/res/values/colors_greenapple.xml | 51 +- .../main/res/values/colors_midnightdusk.xml | 19 +- .../src/main/res/values/colors_nord.xml | 15 +- .../src/main/res/values/colors_strawberry.xml | 51 +- .../src/main/res/values/colors_tachiyomi.xml | 45 +- .../src/main/res/values/colors_tako.xml | 15 +- .../main/res/values/colors_tealturqoise.xml | 18 +- .../src/main/res/values/colors_tidalwave.xml | 18 +- .../src/main/res/values/colors_yinyang.xml | 14 +- .../src/main/res/values/colors_yotsuba.xml | 16 +- presentation-widget/.gitignore | 1 - presentation-widget/build.gradle.kts | 1 + .../src/main/AndroidManifest.xml | 2 +- .../entries/anime/AnimeWidgetManager.kt | 12 +- .../entries/manga/MangaWidgetManager.kt | 12 +- .../appwidget_coverscreen_background.xml | 2 +- .../layout/appwidget_coverscreen_loading.xml | 2 +- .../src/main/res/values/colors_appwidget.xml | 2 +- .../src/main/res/values/dimens.xml | 2 +- .../updates_grid_lockscreen_widget_info.xml | 2 +- ...updates_grid_samsung_cover_widget_info.xml | 2 +- settings.gradle.kts | 11 +- source-api/.gitignore | 1 - .../src/androidMain/AndroidManifest.xml | 2 +- .../tachiyomi/animesource/model/SAnime.kt | 2 + .../tachiyomi/animesource/model/SAnimeImpl.kt | 2 + .../tachiyomi/animesource/model/SEpisode.kt | 2 + .../animesource/model/SEpisodeImpl.kt | 2 + .../animesource/online/AnimeHttpSource.kt | 21 +- .../kanade/tachiyomi/source/model/SChapter.kt | 2 + .../tachiyomi/source/model/SChapterImpl.kt | 2 + .../kanade/tachiyomi/source/model/SManga.kt | 2 + .../tachiyomi/source/model/SMangaImpl.kt | 2 + .../tachiyomi/source/online/HttpSource.kt | 21 +- source-local/.gitignore | 1 - source-local/build.gradle.kts | 1 + .../src/androidMain/AndroidManifest.xml | 2 +- .../local/entries/anime/LocalAnimeSource.kt | 18 +- .../local/entries/manga/LocalMangaSource.kt | 21 +- .../{EpubFile.kt => EpubReaderExtensions.kt} | 4 +- 698 files changed, 6742 insertions(+), 6533 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 app/.gitignore create mode 100644 app/src/debug/res/mipmap/ic_launcher_round.xml create mode 100644 app/src/main/java/eu/kanade/domain/track/model/AutoTrackState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt rename app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/{CategoriesBackupCreator.kt => AnimeCategoriesBackupCreator.kt} (57%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeExtensionRepoBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeSourcesBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaCategoriesBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaExtensionRepoBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaSourcesBackupCreator.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt rename app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/{CategoriesRestorer.kt => AnimeCategoriesRestorer.kt} (52%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeExtensionRepoRestorer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaCategoriesRestorer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaExtensionRepoRestorer.kt rename app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/{RpcExternalAsset.kt => RPCExternalAsset.kt} (96%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddEntry.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAnime.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/{JellyfinModels.kt => dto/JFItem.kt} (59%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddEntry.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Context.kt => MUContext.kt} (91%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Image.kt => MUImage.kt} (78%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{ListItem.kt => MUListItem.kt} (79%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Rating.kt => MURating.kt} (79%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Record.kt => MURecord.kt} (91%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Series.kt => MUSeries.kt} (89%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Status.kt => MUStatus.kt} (89%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/{Url.kt => MUUrl.kt} (90%) rename app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/{MyAnimeListModels.kt => MyAnimeListUtils.kt} (74%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/{ShikimoriModels.kt => ShikimoriUtils.kt} (78%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddEntryResponse.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMEntry.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt rename app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/{SimklModels.kt => SimklUtils.kt} (82%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSearch.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncWatched.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklUser.kt create mode 100644 app/src/main/res/mipmap/ic_launcher_round.xml delete mode 100644 buildSrc/.gitignore delete mode 100644 buildSrc/src/main/kotlin/mihon.code.detekt.gradle.kts create mode 100644 buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts rename buildSrc/src/main/kotlin/mihon/buildlogic/tasks/{LocalesConfigPlugin.kt => LocalesConfigTask.kt} (82%) delete mode 100644 config/detekt/baseline.xml delete mode 100644 config/detekt/detekt.yml delete mode 100644 core-metadata/.gitignore create mode 100644 core/archive/build.gradle.kts create mode 100644 core/archive/src/main/AndroidManifest.xml rename core/{common/src/main/java/mihon/core/common => archive/src/main/kotlin/mihon/core}/archive/ArchiveEntry.kt (67%) rename core/{common/src/main/java/mihon/core/common => archive/src/main/kotlin/mihon/core}/archive/ArchiveInputStream.kt (94%) rename core/{common/src/main/java/mihon/core/common => archive/src/main/kotlin/mihon/core}/archive/ArchiveReader.kt (55%) rename core/{common/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt => archive/src/main/kotlin/mihon/core/archive/EpubReader.kt} (95%) create mode 100644 core/archive/src/main/kotlin/mihon/core/archive/UniFileExtensions.kt rename core/{common/src/main/java/mihon/core/common => archive/src/main/kotlin/mihon/core}/archive/ZipWriter.kt (88%) delete mode 100644 core/common/.gitignore delete mode 100644 core/common/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt delete mode 100644 data/.gitignore delete mode 100644 domain/.gitignore create mode 100644 domain/src/main/java/mihon/domain/items/chapter/interactor/FilterChaptersForDownload.kt create mode 100644 domain/src/main/java/mihon/domain/items/episode/interactor/FilterEpisodesForDownload.kt delete mode 100644 i18n/.gitignore create mode 100644 i18n/src/main/AndroidManifest.xml delete mode 100644 macrobenchmark/.gitignore delete mode 100644 presentation-core/.gitignore create mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Slider.kt delete mode 100644 presentation-widget/.gitignore delete mode 100644 source-api/.gitignore delete mode 100644 source-local/.gitignore rename source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/{EpubFile.kt => EpubReaderExtensions.kt} (92%) 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 8bea81463a..226ad9d253 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -1,13 +1,19 @@ 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/main/res/**/strings-animetail.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 }} @@ -23,33 +29,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 db2527ffb6..676ff060c1 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: dorny/paths-filter@v2 id: filter @@ -68,10 +68,10 @@ jobs: - 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 @@ -83,19 +83,20 @@ jobs: contents: ${{ secrets.CLIENT_SECRETS_TEXT }} write-mode: overwrite + - 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 @@ -148,7 +149,7 @@ jobs: - name: Create Release if: startsWith(github.ref, 'refs/tags/') && github.repository == 'dark25/Animetail2' - 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: Animetail ${{ env.VERSION_TAG }} diff --git a/.gitignore b/.gitignore index 485bfd2887..522c384408 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,17 @@ +# Build files .gradle .kotlin -/local.properties -/acra.properties -/.idea/workspace.xml -.DS_Store +build + +# IDE files +*.iml .idea/* !.idea/icon.png -*iml -*.iml +/captures app/**/assets/client_secrets.json -# 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 f8404c1284..12e583780f 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/s82Vu589Ya) for online help and to ask questions while developing. diff --git a/README.md b/README.md index ce127624c7..6f0b107fd2 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Features include: * Local reading/watching of downloaded 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), and [Bangumi](https://bgm.tv/) + * Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) * Categories to organize your library * Light and dark themes * Create backups locally to read/watch offline or to your desired cloud service 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 693ccfae00..69f0a9879b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,9 +6,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 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) +} + +if (gradle.startParameter.taskRequests.toString().contains("Standard")) { + apply() } if (gradle.startParameter.taskRequests.toString().contains("Standard")) { @@ -17,6 +21,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) { shortcutHelper.setFilePath("./shortcuts.xml") +@Suppress("PropertyName") val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { @@ -106,13 +111,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", ), ) } @@ -139,6 +147,7 @@ android { dependencies { implementation(projects.i18n) + implementation(projects.core.archive) implementation(projects.core.common) implementation(projects.coreMetadata) implementation(projects.sourceApi) @@ -158,7 +167,6 @@ dependencies { debugImplementation(compose.ui.tooling) implementation(compose.ui.tooling.preview) implementation(compose.ui.util) - implementation(compose.accompanist.systemuicontroller) implementation(compose.colorpicker) implementation(androidx.interpolator) @@ -246,7 +254,6 @@ dependencies { implementation(libs.compose.webview) implementation(libs.compose.grid) - implementation(libs.google.api.services.drive) implementation(libs.google.api.client.oauth) @@ -279,7 +286,7 @@ dependencies { // true type parser implementation(libs.truetypeparser) // torrserver - implementation(libs.torrentserver) + implementation(libs.torrentserver) // Cast implementation(libs.bundles.cast) } diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index 0f65411b9a..ce4c570849 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/debug/res/mipmap/ic_launcher.xml b/app/src/debug/res/mipmap/ic_launcher.xml index 0f65411b9a..ce4c570849 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 new file mode 100644 index 0000000000..4f4ecc0d2e --- /dev/null +++ b/app/src/debug/res/mipmap/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + 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..3cff31fa00 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -58,6 +58,8 @@ 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 @@ -284,6 +286,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 +297,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()) } 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 e7115e573e..d5248d803b 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 colorTheme() = preferenceStore.getInt("pref_color_theme", 0) 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/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/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/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/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 ef9450500e..c65db99144 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 @@ -68,9 +68,9 @@ import eu.kanade.presentation.entries.components.EntryToolbar import eu.kanade.presentation.entries.components.ItemHeader import eu.kanade.presentation.entries.components.MissingItemCountListItem import eu.kanade.presentation.util.formatEpisodeNumber +import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.SAnime -import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo @@ -461,13 +461,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, ) @@ -533,7 +529,8 @@ private fun AnimeScreenSmallImpl( timer -= 1000L } } - if (timer > 0L && showNextEpisodeAirTime && + if (timer > 0L && + showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED ) { NextEpisodeAiringListItem( @@ -756,13 +753,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, ) @@ -829,7 +822,8 @@ fun AnimeScreenLargeImpl( timer -= 1000L } } - if (timer > 0L && showNextEpisodeAirTime && + if (timer > 0L && + showNextEpisodeAirTime && state.anime.status.toInt() != SAnime.COMPLETED ) { NextEpisodeAiringListItem( @@ -1069,6 +1063,7 @@ private fun formatTime(milliseconds: Long, useDayFormat: Boolean = false): Strin ) } } + // AM (FILE_SIZE) --> private val animeDownloadProvider: AnimeDownloadProvider by injectLazy() // <-- AM (FILE_SIZE) 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 587233932a..4b92d2b04c 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 @@ -2,7 +2,6 @@ package eu.kanade.presentation.entries.anime import android.content.Context import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize @@ -61,14 +60,13 @@ import com.google.android.gms.cast.MediaMetadata import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaStatus import com.google.android.gms.cast.framework.CastContext -import com.google.android.gms.cast.framework.CastSession -import com.google.android.gms.cast.framework.SessionManagerListener import com.google.android.gms.common.images.WebImage import eu.kanade.presentation.components.TabbedDialogPaddings 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.loader.EpisodeLoader +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -87,7 +85,6 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences private val playerPreferences = Injekt.get() class EpisodeOptionsDialogScreen( @@ -286,17 +283,24 @@ private fun VideoList( ) } }, - //start tail cast + // start tail cast onCastClicked = { scope.launch { if (playerPreferences.enableCast().get()) { - sendChaptersToCast(context, anime.title, episode.name, episode.lastSecondSeen, anime.thumbnailUrl.orEmpty(), selectedVideo.videoUrl!!) + sendChaptersToCast( + context, + anime.title, + episode.name, + episode.lastSecondSeen, + anime.thumbnailUrl.orEmpty(), + selectedVideo.videoUrl!!, + ) } else { context.toast("Cast is disabled") } } }, - //end tail cast + // end tail cast ) } } @@ -387,7 +391,6 @@ private fun QualityOptions( }, ) } - } @Composable @@ -436,7 +439,14 @@ private fun ClickableRow( // Start tail cast -private fun sendChaptersToCast(context: Context, title: String, episode: String, lastSecondSeen: Long, image: String, videoUrl: String) { +private fun sendChaptersToCast( + context: Context, + title: String, + episode: String, + lastSecondSeen: Long, + image: String, + videoUrl: String, +) { val castSession = CastContext.getSharedInstance(context).sessionManager.currentCastSession val remoteMediaClient = castSession?.remoteMediaClient if (castSession == null || !castSession.isConnected) { @@ -475,4 +485,4 @@ private fun sendChaptersToCast(context: Context, title: String, episode: String, } } -// End tail cast +// End tail cast0 diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeCoverDialog.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeCoverDialog.kt index 901235c98f..2130d348d2 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeCoverDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeCoverDialog.kt @@ -56,7 +56,7 @@ import tachiyomi.presentation.core.util.clickableNoIndication @Composable fun AnimeCoverDialog( - coverDataProvider: () -> Anime, + anime: Anime, isCustomCover: Boolean, snackbarHostState: SnackbarHostState, onShareClick: () -> Unit, @@ -168,7 +168,7 @@ fun AnimeCoverDialog( }, update = { view -> val request = ImageRequest.Builder(view.context) - .data(coverDataProvider()) + .data(anime) .size(Size.ORIGINAL) .memoryCachePolicy(CachePolicy.DISABLED) .target { image -> diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt index 5acd992057..d297b7fcef 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeEpisodeListItem.kt @@ -44,8 +44,8 @@ import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload import me.saket.swipe.SwipeableActionsBox import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.ReadItemAlpha -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA +import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground @@ -139,7 +139,7 @@ fun AnimeEpisodeListItem( maxLines = 1, overflow = TextOverflow.Ellipsis, onTextLayout = { textHeight = it.size.height }, - color = LocalContentColor.current.copy(alpha = if (seen) ReadItemAlpha else 1f), + color = LocalContentColor.current.copy(alpha = if (seen) DISABLED_ALPHA else 1f), ) } @@ -147,7 +147,7 @@ fun AnimeEpisodeListItem( val subtitleStyle = MaterialTheme.typography.bodySmall .merge( color = LocalContentColor.current - .copy(alpha = if (seen) ReadItemAlpha else SecondaryItemAlpha) + .copy(alpha = if (seen) DISABLED_ALPHA else SECONDARY_ALPHA), ) ProvideTextStyle(value = subtitleStyle) { if (date != null) { @@ -163,7 +163,7 @@ fun AnimeEpisodeListItem( text = watchProgress, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = LocalContentColor.current.copy(alpha = ReadItemAlpha), + color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA), ) if (scanlator != null) DotSeparatorText() } @@ -244,12 +244,12 @@ fun NextEpisodeAiringListItem( maxLines = 1, overflow = TextOverflow.Ellipsis, onTextLayout = { textHeight = it.size.height }, - modifier = Modifier.alpha(SecondaryItemAlpha), + modifier = Modifier.alpha(SECONDARY_ALPHA), color = MaterialTheme.colorScheme.primary, ) } Spacer(modifier = Modifier.height(6.dp)) - Row(modifier = Modifier.alpha(SecondaryItemAlpha)) { + Row(modifier = Modifier.alpha(SECONDARY_ALPHA)) { ProvideTextStyle( value = MaterialTheme.typography.bodyMedium.copy(fontSize = 12.sp), ) { diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt index 0d2c9317bc..95c12c9c1e 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/AnimeInfoHeader.kt @@ -2,6 +2,7 @@ package eu.kanade.presentation.entries.anime.components import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector @@ -74,6 +75,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.entries.components.DotSeparatorText import eu.kanade.presentation.entries.components.ItemCover @@ -82,6 +85,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.pluralStringResource @@ -98,13 +102,9 @@ private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTIL fun AnimeInfoBox( isTabletUi: Boolean, appBarPadding: Dp, - title: String, - author: String?, - artist: String?, + anime: Anime, sourceName: String, isStubSource: Boolean, - coverDataProvider: () -> Anime, - status: Long, onCoverClick: () -> Unit, doSearch: (query: String, global: Boolean) -> Unit, modifier: Modifier = Modifier, @@ -116,7 +116,10 @@ fun AnimeInfoBox( MaterialTheme.colorScheme.background, ) AsyncImage( - model = coverDataProvider(), + model = ImageRequest.Builder(LocalContext.current) + .data(anime) + .crossfade(true) + .build(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -136,28 +139,20 @@ fun AnimeInfoBox( if (!isTabletUi) { AnimeAndSourceTitlesSmall( appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + anime = anime, sourceName = sourceName, isStubSource = isStubSource, + onCoverClick = onCoverClick, + doSearch = doSearch, ) } else { AnimeAndSourceTitlesLarge( appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + anime = anime, sourceName = sourceName, isStubSource = isStubSource, + onCoverClick = onCoverClick, + doSearch = doSearch, ) } } @@ -178,7 +173,7 @@ fun AnimeActionRow( onEditCategory: (() -> Unit)?, modifier: Modifier = Modifier, ) { - val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) + val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA) // TODO: show something better when using custom interval val nextUpdateDays = remember(nextUpdate) { @@ -276,7 +271,8 @@ fun ExpandableAnimeDescription( modifier = Modifier .padding(top = 8.dp) .padding(vertical = 12.dp) - .animateContentSize(), + .animateContentSize(animationSpec = spring()) + .fillMaxWidth(), ) { var showMenu by remember { mutableStateOf(false) } var tagSelected by remember { mutableStateOf("") } @@ -340,15 +336,11 @@ fun ExpandableAnimeDescription( @Composable private fun AnimeAndSourceTitlesLarge( appBarPadding: Dp, - coverDataProvider: () -> Anime, - onCoverClick: () -> Unit, - title: String, - doSearch: (query: String, global: Boolean) -> Unit, - author: String?, - artist: String?, - status: Long, + anime: Anime, sourceName: String, isStubSource: Boolean, + onCoverClick: () -> Unit, + doSearch: (query: String, global: Boolean) -> Unit, ) { Column( modifier = Modifier @@ -358,19 +350,22 @@ private fun AnimeAndSourceTitlesLarge( ) { ItemCover.Book( modifier = Modifier.fillMaxWidth(0.65f), - data = coverDataProvider(), + data = ImageRequest.Builder(LocalContext.current) + .data(anime) + .crossfade(true) + .build(), contentDescription = stringResource(MR.strings.manga_cover), onClick = onCoverClick, ) Spacer(modifier = Modifier.height(16.dp)) AnimeContentInfo( - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + title = anime.title, + author = anime.author, + artist = anime.artist, + status = anime.status, sourceName = sourceName, isStubSource = isStubSource, + doSearch = doSearch, textAlign = TextAlign.Center, ) } @@ -379,15 +374,11 @@ private fun AnimeAndSourceTitlesLarge( @Composable private fun AnimeAndSourceTitlesSmall( appBarPadding: Dp, - coverDataProvider: () -> Anime, - onCoverClick: () -> Unit, - title: String, - doSearch: (query: String, global: Boolean) -> Unit, - author: String?, - artist: String?, - status: Long, + anime: Anime, sourceName: String, isStubSource: Boolean, + onCoverClick: () -> Unit, + doSearch: (query: String, global: Boolean) -> Unit, ) { Row( modifier = Modifier @@ -400,7 +391,10 @@ private fun AnimeAndSourceTitlesSmall( modifier = Modifier .sizeIn(maxWidth = 100.dp) .align(Alignment.Top), - data = coverDataProvider(), + data = ImageRequest.Builder(LocalContext.current) + .data(anime) + .crossfade(true) + .build(), contentDescription = stringResource(MR.strings.manga_cover), onClick = onCoverClick, ) @@ -408,13 +402,13 @@ private fun AnimeAndSourceTitlesSmall( verticalArrangement = Arrangement.spacedBy(2.dp), ) { AnimeContentInfo( - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + title = anime.title, + author = anime.author, + artist = anime.artist, + status = anime.status, sourceName = sourceName, isStubSource = isStubSource, + doSearch = doSearch, ) } } @@ -423,12 +417,12 @@ private fun AnimeAndSourceTitlesSmall( @Composable private fun ColumnScope.AnimeContentInfo( title: String, - doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, + doSearch: (query: String, global: Boolean) -> Unit, textAlign: TextAlign? = LocalTextStyle.current.textAlign, ) { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/presentation/entries/anime/components/EpisodeDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/entries/anime/components/EpisodeDownloadIndicator.kt index 7e6ca858df..af26ef1476 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/anime/components/EpisodeDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/anime/components/EpisodeDownloadIndicator.kt @@ -251,7 +251,7 @@ private fun DownloadedIndicator( // AM (FILE_SIZE) --> private fun formatFileSize(fileSize: Long): String { val megaByteSize = fileSize / 1000.0 / 1000.0 - return if (megaByteSize > 900){ + return if (megaByteSize > 900) { val gigaByteSize = megaByteSize / 1000.0 "${BigDecimal(gigaByteSize).setScale(2, RoundingMode.HALF_EVEN)} GB" } else { @@ -260,7 +260,6 @@ private fun formatFileSize(fileSize: Long): String { } // <-- AM (FILE_SIZE) - @Composable private fun ErrorIndicator( enabled: Boolean, diff --git a/app/src/main/java/eu/kanade/presentation/entries/components/ItemHeader.kt b/app/src/main/java/eu/kanade/presentation/entries/components/ItemHeader.kt index cdfcd52906..9c6e75e53e 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/components/ItemHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/components/ItemHeader.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import tachiyomi.i18n.MR -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.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource @@ -63,6 +63,6 @@ private fun MissingItemsWarning(count: Int) { maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error.copy(alpha = SecondaryItemAlpha), + color = MaterialTheme.colorScheme.error.copy(alpha = SECONDARY_ALPHA), ) } diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt index 48d49ba27e..986dbd19c1 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/MangaScreen.kt @@ -415,13 +415,9 @@ private fun MangaScreenSmallImpl( MangaInfoBox( isTabletUi = false, appBarPadding = topPadding, - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, + manga = state.manga, sourceName = remember { state.source.getNameForMangaInfo() }, isStubSource = remember { state.source is StubMangaSource }, - coverDataProvider = { state.manga }, - status = state.manga.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) @@ -670,13 +666,9 @@ fun MangaScreenLargeImpl( MangaInfoBox( isTabletUi = true, appBarPadding = contentPadding.calculateTopPadding(), - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, + manga = state.manga, sourceName = remember { state.source.getNameForMangaInfo() }, isStubSource = remember { state.source is StubMangaSource }, - coverDataProvider = { state.manga }, - status = state.manga.status, onCoverClick = onCoverClicked, doSearch = onSearch, ) diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt index 79a1da0705..f3e881449c 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaChapterListItem.kt @@ -41,8 +41,8 @@ import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload import me.saket.swipe.SwipeableActionsBox import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.ReadItemAlpha -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA +import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground @@ -134,7 +134,7 @@ fun MangaChapterListItem( maxLines = 1, overflow = TextOverflow.Ellipsis, onTextLayout = { textHeight = it.size.height }, - color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f), + color = LocalContentColor.current.copy(alpha = if (read) DISABLED_ALPHA else 1f), ) } @@ -142,7 +142,7 @@ fun MangaChapterListItem( val subtitleStyle = MaterialTheme.typography.bodySmall .merge( color = LocalContentColor.current - .copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha) + .copy(alpha = if (read) DISABLED_ALPHA else SECONDARY_ALPHA), ) ProvideTextStyle(value = subtitleStyle) { if (date != null) { @@ -158,7 +158,7 @@ fun MangaChapterListItem( text = readProgress, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = LocalContentColor.current.copy(alpha = ReadItemAlpha), + color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA), ) if (scanlator != null) DotSeparatorText() } diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaCoverDialog.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaCoverDialog.kt index eabe209ce2..d88de54d87 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaCoverDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaCoverDialog.kt @@ -56,7 +56,7 @@ import tachiyomi.presentation.core.util.clickableNoIndication @Composable fun MangaCoverDialog( - coverDataProvider: () -> Manga, + manga: Manga, isCustomCover: Boolean, snackbarHostState: SnackbarHostState, onShareClick: () -> Unit, @@ -168,7 +168,7 @@ fun MangaCoverDialog( }, update = { view -> val request = ImageRequest.Builder(view.context) - .data(coverDataProvider()) + .data(manga) .size(Size.ORIGINAL) .memoryCachePolicy(CachePolicy.DISABLED) .target { image -> diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt index 56d79df5f1..2b8824e6c5 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/MangaInfoHeader.kt @@ -2,6 +2,7 @@ package eu.kanade.presentation.entries.manga.components import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector @@ -74,6 +75,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.entries.components.DotSeparatorText import eu.kanade.presentation.entries.components.ItemCover @@ -82,6 +85,7 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.pluralStringResource @@ -98,13 +102,9 @@ private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTIL fun MangaInfoBox( isTabletUi: Boolean, appBarPadding: Dp, - title: String, - author: String?, - artist: String?, + manga: Manga, sourceName: String, isStubSource: Boolean, - coverDataProvider: () -> Manga, - status: Long, onCoverClick: () -> Unit, doSearch: (query: String, global: Boolean) -> Unit, modifier: Modifier = Modifier, @@ -116,7 +116,10 @@ fun MangaInfoBox( MaterialTheme.colorScheme.background, ) AsyncImage( - model = coverDataProvider(), + model = ImageRequest.Builder(LocalContext.current) + .data(manga) + .crossfade(true) + .build(), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -136,28 +139,20 @@ fun MangaInfoBox( if (!isTabletUi) { MangaAndSourceTitlesSmall( appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + manga = manga, sourceName = sourceName, isStubSource = isStubSource, + onCoverClick = onCoverClick, + doSearch = doSearch, ) } else { MangaAndSourceTitlesLarge( appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + manga = manga, sourceName = sourceName, isStubSource = isStubSource, + onCoverClick = onCoverClick, + doSearch = doSearch, ) } } @@ -178,7 +173,7 @@ fun MangaActionRow( onEditCategory: (() -> Unit)?, modifier: Modifier = Modifier, ) { - val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) + val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA) // TODO: show something better when using custom interval val nextUpdateDays = remember(nextUpdate) { @@ -275,7 +270,8 @@ fun ExpandableMangaDescription( modifier = Modifier .padding(top = 8.dp) .padding(vertical = 12.dp) - .animateContentSize(), + .animateContentSize(animationSpec = spring()) + .fillMaxWidth(), ) { var showMenu by remember { mutableStateOf(false) } var tagSelected by remember { mutableStateOf("") } @@ -339,15 +335,11 @@ fun ExpandableMangaDescription( @Composable private fun MangaAndSourceTitlesLarge( appBarPadding: Dp, - coverDataProvider: () -> Manga, - onCoverClick: () -> Unit, - title: String, - doSearch: (query: String, global: Boolean) -> Unit, - author: String?, - artist: String?, - status: Long, + manga: Manga, sourceName: String, isStubSource: Boolean, + onCoverClick: () -> Unit, + doSearch: (query: String, global: Boolean) -> Unit, ) { Column( modifier = Modifier @@ -357,19 +349,22 @@ private fun MangaAndSourceTitlesLarge( ) { ItemCover.Book( modifier = Modifier.fillMaxWidth(0.65f), - data = coverDataProvider(), + data = ImageRequest.Builder(LocalContext.current) + .data(manga) + .crossfade(true) + .build(), contentDescription = stringResource(MR.strings.manga_cover), onClick = onCoverClick, ) Spacer(modifier = Modifier.height(16.dp)) MangaContentInfo( - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + title = manga.title, + author = manga.author, + artist = manga.artist, + status = manga.status, sourceName = sourceName, isStubSource = isStubSource, + doSearch = doSearch, textAlign = TextAlign.Center, ) } @@ -378,15 +373,11 @@ private fun MangaAndSourceTitlesLarge( @Composable private fun MangaAndSourceTitlesSmall( appBarPadding: Dp, - coverDataProvider: () -> Manga, - onCoverClick: () -> Unit, - title: String, - doSearch: (query: String, global: Boolean) -> Unit, - author: String?, - artist: String?, - status: Long, + manga: Manga, sourceName: String, isStubSource: Boolean, + onCoverClick: () -> Unit, + doSearch: (query: String, global: Boolean) -> Unit, ) { Row( modifier = Modifier @@ -399,7 +390,10 @@ private fun MangaAndSourceTitlesSmall( modifier = Modifier .sizeIn(maxWidth = 100.dp) .align(Alignment.Top), - data = coverDataProvider(), + data = ImageRequest.Builder(LocalContext.current) + .data(manga) + .crossfade(true) + .build(), contentDescription = stringResource(MR.strings.manga_cover), onClick = onCoverClick, ) @@ -407,13 +401,13 @@ private fun MangaAndSourceTitlesSmall( verticalArrangement = Arrangement.spacedBy(2.dp), ) { MangaContentInfo( - title = title, - doSearch = doSearch, - author = author, - artist = artist, - status = status, + title = manga.title, + author = manga.author, + artist = manga.artist, + status = manga.status, sourceName = sourceName, isStubSource = isStubSource, + doSearch = doSearch, ) } } @@ -422,12 +416,12 @@ private fun MangaAndSourceTitlesSmall( @Composable private fun ColumnScope.MangaContentInfo( title: String, - doSearch: (query: String, global: Boolean) -> Unit, author: String?, artist: String?, status: Long, sourceName: String, isStubSource: Boolean, + doSearch: (query: String, global: Boolean) -> Unit, textAlign: TextAlign? = LocalTextStyle.current.textAlign, ) { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/presentation/entries/manga/components/ScanlatorFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/entries/manga/components/ScanlatorFilterDialog.kt index aef644c6c0..ae905e943f 100644 --- a/app/src/main/java/eu/kanade/presentation/entries/manga/components/ScanlatorFilterDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/entries/manga/components/ScanlatorFilterDialog.kt @@ -108,8 +108,14 @@ fun ScanlatorFilterDialog( } } else { FlowRow { - TextButton(onClick = mutableExcludedScanlators::clear) { - Text(text = stringResource(MR.strings.action_reset)) + if (mutableExcludedScanlators.isEmpty()) { + TextButton(onClick = { mutableExcludedScanlators.addAll(availableScanlators) }) { + Text(text = stringResource(MR.strings.action_select_all)) + } + } else { + TextButton(onClick = mutableExcludedScanlators::clear) { + Text(text = stringResource(MR.strings.action_reset)) + } } Spacer(modifier = Modifier.weight(1f)) TextButton(onClick = onDismissRequest) { diff --git a/app/src/main/java/eu/kanade/presentation/history/anime/AnimeHistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/anime/AnimeHistoryScreen.kt index 0fb37b7a89..6afb689a37 100644 --- a/app/src/main/java/eu/kanade/presentation/history/anime/AnimeHistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/anime/AnimeHistoryScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import eu.kanade.presentation.components.relativeDateTimeText import eu.kanade.presentation.history.anime.components.AnimeHistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations import tachiyomi.i18n.MR @@ -84,14 +85,14 @@ private fun AnimeHistoryScreenContent( when (item) { is AnimeHistoryUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), text = relativeDateTimeText(item.date), ) } is AnimeHistoryUiModel.Item -> { val value = item.item AnimeHistoryItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), history = value, onClickCover = { onClickCover(value) }, onClickResume = { onClickResume(value) }, diff --git a/app/src/main/java/eu/kanade/presentation/history/manga/MangaHistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/manga/MangaHistoryScreen.kt index 3f85f9e42c..a11d5490bf 100644 --- a/app/src/main/java/eu/kanade/presentation/history/manga/MangaHistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/manga/MangaHistoryScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import eu.kanade.presentation.components.relativeDateTimeText import eu.kanade.presentation.history.manga.components.MangaHistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.history.manga.MangaHistoryScreenModel import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations import tachiyomi.i18n.MR @@ -84,14 +85,14 @@ private fun MangaHistoryScreenContent( when (item) { is MangaHistoryUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), text = relativeDateTimeText(item.date), ) } is MangaHistoryUiModel.Item -> { val value = item.item MangaHistoryItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), history = value, onClickCover = { onClickCover(value) }, onClickResume = { onClickResume(value) }, diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt index 58d7afc964..31206faf9d 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibrarySettingsDialog.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -32,6 +34,7 @@ import tachiyomi.domain.library.anime.model.sort import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.BaseSortItem import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.IconItem @@ -197,23 +200,39 @@ private fun ColumnScope.SortPage( }.not() // SY <-- - val trackerSortOption = if (trackers.isEmpty()) { - emptyList() - } else { - listOf(MR.strings.action_sort_tracker_score to AnimeLibrarySort.Type.TrackerMean) + val options = remember(trackers.isEmpty()) { + val trackerMeanPair = if (trackers.isNotEmpty()) { + MR.strings.action_sort_tracker_score to AnimeLibrarySort.Type.TrackerMean + } else { + null + } + listOfNotNull( + MR.strings.action_sort_alpha to AnimeLibrarySort.Type.Alphabetical, + MR.strings.action_sort_total to AnimeLibrarySort.Type.TotalEpisodes, + MR.strings.action_sort_last_read to AnimeLibrarySort.Type.LastSeen, + MR.strings.action_sort_last_manga_update to AnimeLibrarySort.Type.LastUpdate, + MR.strings.action_sort_unread_count to AnimeLibrarySort.Type.UnseenCount, + MR.strings.action_sort_latest_chapter to AnimeLibrarySort.Type.LatestEpisode, + MR.strings.action_sort_chapter_fetch_date to AnimeLibrarySort.Type.EpisodeFetchDate, + MR.strings.action_sort_date_added to AnimeLibrarySort.Type.DateAdded, + trackerMeanPair, + MR.strings.action_sort_airing_time to AnimeLibrarySort.Type.AiringTime, + MR.strings.action_sort_random to AnimeLibrarySort.Type.Random, + ) } - listOf( - MR.strings.action_sort_alpha to AnimeLibrarySort.Type.Alphabetical, - MR.strings.action_sort_total_episodes to AnimeLibrarySort.Type.TotalEpisodes, - MR.strings.action_sort_last_seen to AnimeLibrarySort.Type.LastSeen, - MR.strings.action_sort_last_anime_update to AnimeLibrarySort.Type.LastUpdate, - MR.strings.action_sort_unseen_count to AnimeLibrarySort.Type.UnseenCount, - MR.strings.action_sort_latest_episode to AnimeLibrarySort.Type.LatestEpisode, - MR.strings.action_sort_episode_fetch_date to AnimeLibrarySort.Type.EpisodeFetchDate, - MR.strings.action_sort_date_added to AnimeLibrarySort.Type.DateAdded, - MR.strings.action_sort_airing_time to AnimeLibrarySort.Type.AiringTime, - ).plus(trackerSortOption).map { (titleRes, mode) -> + options.map { (titleRes, mode) -> + if (mode == AnimeLibrarySort.Type.Random) { + BaseSortItem( + label = stringResource(titleRes), + icon = Icons.Default.Refresh + .takeIf { sortingMode == AnimeLibrarySort.Type.Random }, + onClick = { + screenModel.setSort(category, mode, AnimeLibrarySort.Direction.Ascending) + }, + ) + return@map + } SortItem( label = stringResource(titleRes), sortDescending = sortDescending.takeIf { sortingMode == mode }, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/CommonEntryItem.kt b/app/src/main/java/eu/kanade/presentation/library/components/CommonEntryItem.kt index 43c9ecd4c6..0252426fb2 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/CommonEntryItem.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/CommonEntryItem.kt @@ -62,7 +62,7 @@ private val ContinueViewingButtonIconSizeLarge = 20.dp private val ContinueViewingButtonGridPadding = 6.dp private val ContinueViewingButtonListSpacing = 8.dp -private const val GridSelectedCoverAlpha = 0.76f +private const val GRID_SELECTED_COVER_ALPHA = 0.76f /** * Layout of grid list item with title overlaying the cover. @@ -90,7 +90,7 @@ fun EntryCompactGridItem( ItemCover.Book( modifier = Modifier .fillMaxWidth() - .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), + .alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha), data = coverData, ) }, @@ -197,7 +197,7 @@ fun EntryComfortableGridItem( ItemCover.Book( modifier = Modifier .fillMaxWidth() - .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), + .alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha), data = coverData, ) }, @@ -392,7 +392,7 @@ private fun ContinueViewingButton( containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f), contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer), ), - modifier = Modifier.size(size) + modifier = Modifier.size(size), ) { Icon( imageVector = Icons.Filled.PlayArrow, diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt index 7a814be717..d0a2ded55d 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibrarySettingsDialog.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -32,6 +34,7 @@ import tachiyomi.domain.library.manga.model.sort import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.BaseSortItem import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.IconItem @@ -197,22 +200,38 @@ private fun ColumnScope.SortPage( }.not() // SY <-- - val trackerSortOption = if (trackers.isEmpty()) { - emptyList() - } else { - listOf(MR.strings.action_sort_tracker_score to MangaLibrarySort.Type.TrackerMean) + val options = remember(trackers.isEmpty()) { + val trackerMeanPair = if (trackers.isNotEmpty()) { + MR.strings.action_sort_tracker_score to MangaLibrarySort.Type.TrackerMean + } else { + null + } + listOfNotNull( + MR.strings.action_sort_alpha to MangaLibrarySort.Type.Alphabetical, + MR.strings.action_sort_total to MangaLibrarySort.Type.TotalChapters, + MR.strings.action_sort_last_read to MangaLibrarySort.Type.LastRead, + MR.strings.action_sort_last_manga_update to MangaLibrarySort.Type.LastUpdate, + MR.strings.action_sort_unread_count to MangaLibrarySort.Type.UnreadCount, + MR.strings.action_sort_latest_chapter to MangaLibrarySort.Type.LatestChapter, + MR.strings.action_sort_chapter_fetch_date to MangaLibrarySort.Type.ChapterFetchDate, + MR.strings.action_sort_date_added to MangaLibrarySort.Type.DateAdded, + trackerMeanPair, + MR.strings.action_sort_random to MangaLibrarySort.Type.Random, + ) } - listOf( - MR.strings.action_sort_alpha to MangaLibrarySort.Type.Alphabetical, - MR.strings.action_sort_total to MangaLibrarySort.Type.TotalChapters, - MR.strings.action_sort_last_read to MangaLibrarySort.Type.LastRead, - MR.strings.action_sort_last_manga_update to MangaLibrarySort.Type.LastUpdate, - MR.strings.action_sort_unread_count to MangaLibrarySort.Type.UnreadCount, - MR.strings.action_sort_latest_chapter to MangaLibrarySort.Type.LatestChapter, - MR.strings.action_sort_chapter_fetch_date to MangaLibrarySort.Type.ChapterFetchDate, - MR.strings.action_sort_date_added to MangaLibrarySort.Type.DateAdded, - ).plus(trackerSortOption).map { (titleRes, mode) -> + options.map { (titleRes, mode) -> + if (mode == MangaLibrarySort.Type.Random) { + BaseSortItem( + label = stringResource(titleRes), + icon = Icons.Default.Refresh + .takeIf { sortingMode == MangaLibrarySort.Type.Random }, + onClick = { + screenModel.setSort(category, mode, MangaLibrarySort.Direction.Ascending) + }, + ) + return@map + } SortItem( label = stringResource(titleRes), sortDescending = sortDescending.takeIf { sortingMode == mode }, diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index 3d561b0c36..87dd1ee4d3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -75,7 +75,7 @@ private fun NewUpdateScreenPreview() { changelogInfo = """ ## Yay Foobar - + ### More info - Hello - World diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt index 934a1e02e8..3785b37865 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt @@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.data.track.Tracker import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.coroutines.CoroutineScope -import tachiyomi.core.common.storage.openFileDescriptor +import mihon.core.archive.openFileDescriptor import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index e022b897ea..9eb3e22d3e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -282,6 +282,7 @@ object SettingsAdvancedScreen : SearchableSettings { try { // OkHttp checks for valid values internally Headers.Builder().add("User-Agent", it) + context.toast(MR.strings.requires_app_restart) } catch (_: IllegalArgumentException) { context.toast(MR.strings.error_user_agent_string_invalid) return@EditTextPreference false @@ -363,7 +364,7 @@ object SettingsAdvancedScreen : SearchableSettings { chooseColorProfile.launch(arrayOf("*/*")) }, ), - ) + ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index e85eecd555..8e55cdeb6f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -81,7 +81,6 @@ import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get - @Suppress("TooManyFunctions") object SettingsDataScreen : SearchableSettings { @@ -334,7 +333,7 @@ object SettingsDataScreen : SearchableSettings { title = stringResource(MR.strings.label_storage), icon = Icons.Outlined.Storage, onClick = { - navigator.push(StorageTab()) + navigator.push(StorageTab) }, ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt index 1b20e23b66..9f18e214e2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt @@ -31,7 +31,6 @@ import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentMap -import kotlinx.coroutines.runBlocking import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.model.Category @@ -54,13 +53,9 @@ object SettingsDownloadScreen : SearchableSettings { @Composable override fun getPreferences(): List { val getMangaCategories = remember { Injekt.get() } - val allMangaCategories by getMangaCategories.subscribe().collectAsState( - initial = runBlocking { getMangaCategories.await() }, - ) + val allMangaCategories by getMangaCategories.subscribe().collectAsState(initial = emptyList()) val getAnimeCategories = remember { Injekt.get() } - val allAnimeCategories by getAnimeCategories.subscribe().collectAsState( - initial = runBlocking { getAnimeCategories.await() }, - ) + val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(initial = emptyList()) val downloadPreferences = remember { Injekt.get() } val basePreferences = remember { Injekt.get() } val speedLimit by downloadPreferences.downloadSpeedLimit().collectAsState() @@ -215,6 +210,7 @@ object SettingsDownloadScreen : SearchableSettings { allMangaCategories: ImmutableList, ): Preference.PreferenceGroup { val downloadNewEpisodesPref = downloadPreferences.downloadNewEpisodes() + val downloadNewUnseenEpisodesOnlyPref = downloadPreferences.downloadNewUnseenEpisodesOnly() val downloadNewEpisodeCategoriesPref = downloadPreferences.downloadNewEpisodeCategories() val downloadNewEpisodeCategoriesExcludePref = downloadPreferences.downloadNewEpisodeCategoriesExclude() @@ -245,6 +241,7 @@ object SettingsDownloadScreen : SearchableSettings { } val downloadNewChaptersPref = downloadPreferences.downloadNewChapters() + val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly() val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories() val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude() @@ -281,6 +278,11 @@ object SettingsDownloadScreen : SearchableSettings { pref = downloadNewEpisodesPref, title = stringResource(MR.strings.pref_download_new_episodes), ), + Preference.PreferenceItem.SwitchPreference( + pref = downloadNewUnseenEpisodesOnlyPref, + title = stringResource(MR.strings.pref_download_new_unseen_episodes_only), + enabled = downloadNewEpisodes, + ), Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.anime_categories), subtitle = getCategoriesLabel( @@ -295,6 +297,11 @@ object SettingsDownloadScreen : SearchableSettings { pref = downloadNewChaptersPref, title = stringResource(MR.strings.pref_download_new), ), + Preference.PreferenceItem.SwitchPreference( + pref = downloadNewUnreadChaptersOnlyPref, + title = stringResource(MR.strings.pref_download_new_unread_chapters_only), + enabled = downloadNewChapters, + ), Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.manga_categories), subtitle = getCategoriesLabel( @@ -370,7 +377,7 @@ object SettingsDownloadScreen : SearchableSettings { } val packageNames = supportedDownloaders.map { it.packageName } val packageNamesReadable = supportedDownloaders - .map { pm.getApplicationLabel(it.applicationInfo).toString() } + .map { pm.getApplicationLabel(it.applicationInfo!!).toString() } val packageNamesMap: Map = mapOf("" to "None") + packageNames.zip(packageNamesReadable).toMap() diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 9e18ef16d8..6b1f04702e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -25,7 +25,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.manga.interactor.ResetMangaCategoryFlags @@ -56,11 +55,9 @@ object SettingsLibraryScreen : SearchableSettings { @Composable override fun getPreferences(): List { val getCategories = remember { Injekt.get() } - val allCategories by getCategories.subscribe() - .collectAsState(initial = runBlocking { getCategories.await() }) + val allCategories by getCategories.subscribe().collectAsState(initial = emptyList()) val getAnimeCategories = remember { Injekt.get() } - val allAnimeCategories by getAnimeCategories.subscribe() - .collectAsState(initial = runBlocking { getAnimeCategories.await() }) + val allAnimeCategories by getAnimeCategories.subscribe().collectAsState(initial = emptyList()) val libraryPreferences = remember { Injekt.get() } return listOf( @@ -87,12 +84,6 @@ object SettingsLibraryScreen : SearchableSettings { val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size val userAnimeCategoriesCount = allAnimeCategories.filterNot(Category::isSystemCategory).size - val defaultCategory by libraryPreferences.defaultMangaCategory().collectAsState() - val selectedCategory = allCategories.find { it.id == defaultCategory.toLong() } - val defaultAnimeCategory by libraryPreferences.defaultAnimeCategory().collectAsState() - val selectedAnimeCategory = - allAnimeCategories.find { it.id == defaultAnimeCategory.toLong() } - // For default category val mangaIds = listOf(libraryPreferences.defaultMangaCategory().defaultValue()) + allCategories.fastMap { it.id.toInt() } @@ -114,13 +105,11 @@ object SettingsLibraryScreen : SearchableSettings { count = userAnimeCategoriesCount, userAnimeCategoriesCount, ), - onClick = { navigator.push(CategoriesTab(false)) }, + onClick = { navigator.push(CategoriesTab) }, ), Preference.PreferenceItem.ListPreference( pref = libraryPreferences.defaultAnimeCategory(), title = stringResource(MR.strings.default_anime_category), - subtitle = selectedAnimeCategory?.visualName - ?: stringResource(MR.strings.default_category_summary), entries = animeIds.zip(animeLabels).toMap().toImmutableMap(), ), Preference.PreferenceItem.TextPreference( @@ -130,12 +119,14 @@ object SettingsLibraryScreen : SearchableSettings { count = userCategoriesCount, userCategoriesCount, ), - onClick = { navigator.push(CategoriesTab(true)) }, + onClick = { + navigator.push(CategoriesTab) + CategoriesTab.showMangaCategory() + }, ), Preference.PreferenceItem.ListPreference( pref = libraryPreferences.defaultMangaCategory(), title = stringResource(MR.strings.default_manga_category), - subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary), entries = mangaIds.zip(mangaLabels).toMap().toImmutableMap(), ), Preference.PreferenceItem.SwitchPreference( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt index 3702e3b7df..88ab7833a1 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt @@ -26,19 +26,19 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.presentation.more.settings.Preference import eu.kanade.tachiyomi.data.torrentServer.TorrentServerPreferences import eu.kanade.tachiyomi.data.torrentServer.service.TorrentServerService -import eu.kanade.tachiyomi.ui.player.Amnis -import eu.kanade.tachiyomi.ui.player.JustPlayer -import eu.kanade.tachiyomi.ui.player.MpvKt -import eu.kanade.tachiyomi.ui.player.MpvKtPreview -import eu.kanade.tachiyomi.ui.player.MpvPlayer -import eu.kanade.tachiyomi.ui.player.MpvRemote -import eu.kanade.tachiyomi.ui.player.MxPlayer -import eu.kanade.tachiyomi.ui.player.MxPlayerFree -import eu.kanade.tachiyomi.ui.player.MxPlayerPro -import eu.kanade.tachiyomi.ui.player.NextPlayer -import eu.kanade.tachiyomi.ui.player.VlcPlayer -import eu.kanade.tachiyomi.ui.player.WebVideoCaster -import eu.kanade.tachiyomi.ui.player.XPlayer +import eu.kanade.tachiyomi.ui.player.AMNIS +import eu.kanade.tachiyomi.ui.player.JUST_PLAYER +import eu.kanade.tachiyomi.ui.player.MPV_KT +import eu.kanade.tachiyomi.ui.player.MPV_KT_PREVIEW +import eu.kanade.tachiyomi.ui.player.MPV_PLAYER +import eu.kanade.tachiyomi.ui.player.MPV_REMOTE +import eu.kanade.tachiyomi.ui.player.MX_PLAYER +import eu.kanade.tachiyomi.ui.player.MX_PLAYER_FREE +import eu.kanade.tachiyomi.ui.player.MX_PLAYER_PRO +import eu.kanade.tachiyomi.ui.player.NEXT_PLAYER +import eu.kanade.tachiyomi.ui.player.VLC_PLAYER +import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER +import eu.kanade.tachiyomi.ui.player.X_PLAYER import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.viewer.AudioChannels import kotlinx.collections.immutable.persistentListOf @@ -398,7 +398,7 @@ object SettingsPlayerScreen : SearchableSettings { val packageNames = supportedPlayers.map { it.packageName } val packageNamesReadable = supportedPlayers - .map { pm.getApplicationLabel(it.applicationInfo).toString() } + .map { pm.getApplicationLabel(it.applicationInfo!!).toString() } val packageNamesMap: Map = packageNames.zip(packageNamesReadable) @@ -518,17 +518,17 @@ object SettingsPlayerScreen : SearchableSettings { } val externalPlayers = listOf( - MpvPlayer, - MxPlayer, - MxPlayerFree, - MxPlayerPro, - VlcPlayer, - MpvKt, - MpvKtPreview, - MpvRemote, - JustPlayer, - NextPlayer, - XPlayer, - WebVideoCaster, - Amnis, + MPV_PLAYER, + MX_PLAYER, + MX_PLAYER_FREE, + MX_PLAYER_PRO, + VLC_PLAYER, + MPV_KT, + MPV_KT_PREVIEW, + MPV_REMOTE, + JUST_PLAYER, + NEXT_PLAYER, + X_PLAYER, + WEB_VIDEO_CASTER, + AMNIS, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt index dc7ae25c57..a1d2c7cc1d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt @@ -13,8 +13,10 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.HorizontalDivider @@ -28,10 +30,8 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,7 +43,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -87,11 +86,7 @@ class SettingsSearchScreen : Screen() { focusRequester.requestFocus() } - var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf( - TextFieldValue(), - ) - } + val textFieldState = rememberTextFieldState() Scaffold( topBar = { Column { @@ -106,22 +101,19 @@ class SettingsSearchScreen : Screen() { }, title = { BasicTextField( - value = textFieldValue, - onValueChange = { textFieldValue = it }, + state = textFieldState, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) .runOnEnterKeyPressed(action = focusManager::clearFocus), textStyle = MaterialTheme.typography.bodyLarge .copy(color = MaterialTheme.colorScheme.onSurface), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { focusManager.clearFocus() }, - ), + onKeyboardAction = { focusManager.clearFocus() }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { - if (textFieldValue.text.isEmpty()) { + decorator = { + if (textFieldState.text.isEmpty()) { Text( text = stringResource(MR.strings.action_search_settings), color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -133,8 +125,8 @@ class SettingsSearchScreen : Screen() { ) }, actions = { - if (textFieldValue.text.isNotEmpty()) { - IconButton(onClick = { textFieldValue = TextFieldValue() }) { + if (textFieldState.text.isNotEmpty()) { + IconButton(onClick = { textFieldState.clearText() }) { Icon( imageVector = Icons.Outlined.Close, contentDescription = null, @@ -149,7 +141,7 @@ class SettingsSearchScreen : Screen() { }, ) { contentPadding -> SearchResult( - searchKey = textFieldValue.text, + searchKey = textFieldState.text.toString(), listState = listState, contentPadding = contentPadding, ) { result -> diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index f3f7ed901d..c513da82ad 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.StringResource +import eu.kanade.domain.track.model.AutoTrackState import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.presentation.more.settings.Preference import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker @@ -55,6 +56,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.withUIContext @@ -90,6 +92,7 @@ object SettingsTrackingScreen : SearchableSettings { val trackerManager = remember { Injekt.get() } val mangaSourceManager = remember { Injekt.get() } val animeSourceManager = remember { Injekt.get() } + val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead() var dialog by remember { mutableStateOf(null) } dialog?.run { @@ -145,6 +148,13 @@ object SettingsTrackingScreen : SearchableSettings { pref = trackPreferences.showNextEpisodeAiringTime(), title = stringResource(MR.strings.pref_show_next_episode_airing_time), ), + Preference.PreferenceItem.ListPreference( + pref = trackPreferences.autoUpdateTrackOnMarkRead(), + title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read), + entries = AutoTrackState.entries + .associateWith { stringResource(it.titleRes) } + .toPersistentMap(), + ), Preference.PreferenceGroup( title = stringResource(MR.strings.services), preferenceItems = persistentListOf( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt index 12e07834b2..0502738e54 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt @@ -36,7 +36,7 @@ class OpenSourceLicensesScreen : Screen() { name = it.name, website = it.website, license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), - ) + ), ) }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/AnimeExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/AnimeExtensionReposScreen.kt index b1f25111a3..163e17b975 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/AnimeExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/AnimeExtensionReposScreen.kt @@ -8,6 +8,7 @@ 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 @@ -32,7 +33,7 @@ class AnimeExtensionReposScreen( val state by screenModel.state.collectAsState() LaunchedEffect(url) { - url?.let { screenModel.createRepo(it) } + url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) } } if (state is RepoScreenState.Loading) { @@ -67,7 +68,6 @@ class AnimeExtensionReposScreen( repo = dialog.repo, ) } - is RepoDialog.Conflict -> { ExtensionRepoConflictDialog( onDismissRequest = screenModel::dismissDialog, @@ -76,6 +76,13 @@ class AnimeExtensionReposScreen( newRepo = dialog.newRepo, ) } + is RepoDialog.Confirm -> { + ExtensionRepoConfirmDialog( + onDismissRequest = screenModel::dismissDialog, + onCreate = { screenModel.createRepo(dialog.url) }, + repo = dialog.url, + ) + } } LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreen.kt index eed2fed712..ce4bfc2ba7 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreen.kt @@ -8,6 +8,7 @@ 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 @@ -32,7 +33,7 @@ class MangaExtensionReposScreen( val state by screenModel.state.collectAsState() LaunchedEffect(url) { - url?.let { screenModel.createRepo(it) } + url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) } } if (state is RepoScreenState.Loading) { @@ -67,7 +68,6 @@ class MangaExtensionReposScreen( repo = dialog.repo, ) } - is RepoDialog.Conflict -> { ExtensionRepoConflictDialog( onDismissRequest = screenModel::dismissDialog, @@ -76,6 +76,13 @@ class MangaExtensionReposScreen( newRepo = dialog.newRepo, ) } + is RepoDialog.Confirm -> { + ExtensionRepoConfirmDialog( + onDismissRequest = screenModel::dismissDialog, + onCreate = { screenModel.createRepo(dialog.url) }, + repo = dialog.url, + ) + } } LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreenModel.kt index c8dee1ff7f..e7ab8dd35e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreenModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/MangaExtensionReposScreenModel.kt @@ -125,6 +125,7 @@ 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 { 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 index 239f917d0b..80cada76ba 100644 --- 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 @@ -152,3 +152,35 @@ fun ExtensionRepoConflictDialog( }, ) } + +@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/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 758be43fa7..7815825433 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState @@ -69,7 +68,7 @@ class CreateBackupScreen : Screen() { LazyColumnWithAction( contentPadding = contentPadding, actionLabel = stringResource(MR.strings.action_create), - actionEnabled = state.options.anyEnabled(), + actionEnabled = state.options.canCreate(), onClickAction = { if (!BackupCreateJob.isManualJobRunning(context)) { try { @@ -110,7 +109,7 @@ class CreateBackupScreen : Screen() { } @Composable - private fun ColumnScope.Options( + private fun Options( options: ImmutableList, state: CreateBackupScreenModel.State, model: CreateBackupScreenModel, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 1e5e36169e..489ff96b4c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -63,7 +63,7 @@ class RestoreBackupScreen( LazyColumnWithAction( contentPadding = contentPadding, actionLabel = stringResource(MR.strings.action_restore), - actionEnabled = state.canRestore && state.options.anyEnabled(), + actionEnabled = state.canRestore && state.options.canRestore(), onClickAction = { model.startRestore() navigator.pop() 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 024ebc5f58..aa1340e0c3 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 @@ -48,7 +48,7 @@ class SyncSettingsSelector : Screen() { LazyColumnWithAction( contentPadding = contentPadding, actionLabel = stringResource(MR.strings.label_sync), - actionEnabled = state.options.anyEnabled(), + actionEnabled = state.options.canCreate(), onClickAction = { if (!SyncDataJob.isRunning(context)) { model.syncNow(context) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt index 4e2d570674..98c9153cdb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/BackupSchemaScreen.kt @@ -28,7 +28,7 @@ import tachiyomi.presentation.core.i18n.stringResource class BackupSchemaScreen : Screen() { companion object { - const val title = "Backup file schema" + const val TITLE = "Backup file schema" } @Composable @@ -45,7 +45,7 @@ class BackupSchemaScreen : Screen() { Scaffold( topBar = { AppBar( - title = title, + title = TITLE, navigateUp = navigator::pop, actions = { AppBarActions( @@ -54,7 +54,7 @@ class BackupSchemaScreen : Screen() { title = stringResource(MR.strings.action_copy_to_clipboard), icon = Icons.Default.ContentCopy, onClick = { - context.copyToClipboard(title, schema) + context.copyToClipboard(TITLE, schema) }, ), ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt index f9de12eabf..f35cb7fd59 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt @@ -31,11 +31,11 @@ class DebugInfoScreen : Screen() { itemsProvider = { listOf( Preference.PreferenceItem.TextPreference( - title = WorkerInfoScreen.title, + title = WorkerInfoScreen.TITLE, onClick = { navigator.push(WorkerInfoScreen()) }, ), Preference.PreferenceItem.TextPreference( - title = BackupSchemaScreen.title, + title = BackupSchemaScreen.TITLE, onClick = { navigator.push(BackupSchemaScreen()) }, ), getAppInfoGroup(), @@ -79,7 +79,7 @@ class DebugInfoScreen : Screen() { val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode value = when (result) { ProfileVerifier.CompilationStatus - .RESULT_CODE_NO_PROFILE, + .RESULT_CODE_NO_PROFILE_INSTALLED, -> "No profile installed" ProfileVerifier.CompilationStatus .RESULT_CODE_COMPILED_WITH_PROFILE, @@ -100,6 +100,7 @@ class DebugInfoScreen : Screen() { ProfileVerifier.CompilationStatus .RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION, -> "Pending compilation" + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED -> "No profile embedded" else -> "Unknown code $result" } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt index e838dd666d..d5207dedf0 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/WorkerInfoScreen.kt @@ -49,7 +49,7 @@ import java.time.ZoneId class WorkerInfoScreen : Screen() { companion object { - const val title = "Worker info" + const val TITLE = "Worker info" } @Composable @@ -65,7 +65,7 @@ class WorkerInfoScreen : Screen() { Scaffold( topBar = { AppBar( - title = title, + title = TITLE, navigateUp = navigator::pop, actions = { AppBarActions( @@ -75,7 +75,7 @@ class WorkerInfoScreen : Screen() { icon = Icons.Default.ContentCopy, onClick = { context.copyToClipboard( - title, + TITLE, enqueued + finished + running, ) }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt index ddad096175..4ef33da62d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt @@ -33,7 +33,9 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource private enum class State { - CHECKED, INVERSED, UNCHECKED + CHECKED, + INVERSED, + UNCHECKED, } @SuppressLint("ComposeParameterOrder") diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt index 8002b3d048..d38643ee12 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import tachiyomi.presentation.core.components.material.SecondaryItemAlpha +import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA import tachiyomi.presentation.core.components.material.padding @Composable @@ -73,7 +73,7 @@ private fun RowScope.BaseStatsItem( style = subtitleStyle .copy( color = MaterialTheme.colorScheme.onSurface - .copy(alpha = SecondaryItemAlpha), + .copy(alpha = SECONDARY_ALPHA), ), textAlign = TextAlign.Center, ) diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 807bd34ff1..b5fa3684c8 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -230,7 +230,7 @@ private fun ChapterText( Text( text = buildAnnotatedString { if (downloaded) { - appendInlineContent(DownloadedIconContentId) + appendInlineContent(DOWNLOADED_ICON_ID) append(' ') } append(name) @@ -240,7 +240,7 @@ private fun ChapterText( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleLarge, inlineContent = persistentMapOf( - DownloadedIconContentId to InlineTextContent( + DOWNLOADED_ICON_ID to InlineTextContent( Placeholder( width = 22.sp, height = 22.sp, @@ -277,7 +277,7 @@ private val CardColor: CardColors ) private val VerticalSpacerSize = 24.dp -private const val DownloadedIconContentId = "downloaded" +private const val DOWNLOADED_ICON_ID = "downloaded" private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy( id = 0L, diff --git a/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt index 46a2aba1e7..b2eea32291 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Photo import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Share @@ -30,9 +31,9 @@ fun ReaderPageActionsDialog( onDismissRequest: () -> Unit, // SY --> onSetAsCover: (useExtraPage: Boolean) -> Unit, - onShare: (useExtraPage: Boolean) -> Unit, + onShare: (copy: Boolean, useExtraPage: Boolean) -> Unit, onSave: (useExtraPage: Boolean) -> Unit, - onShareCombined: () -> Unit, + onShareCombined: (copy: Boolean) -> Unit, onSaveCombined: () -> Unit, hasExtraPage: Boolean, // SY <-- @@ -61,6 +62,24 @@ fun ReaderPageActionsDialog( icon = Icons.Outlined.Photo, onClick = { showSetCoverDialog = true }, ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(MR.strings.action_copy_to_clipboard), + icon = Icons.Outlined.ContentCopy, + onClick = { + onShare(true, false) + onDismissRequest() + }, + ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(MR.strings.action_copy_to_clipboard), + icon = Icons.Outlined.ContentCopy, + onClick = { + onShare(true, false) + onDismissRequest() + }, + ) ActionButton( modifier = Modifier.weight(1f), title = stringResource( @@ -75,7 +94,7 @@ fun ReaderPageActionsDialog( icon = Icons.Outlined.Share, onClick = { // SY --> - onShare(false) + onShare(false, false) // SY <-- onDismissRequest() }, @@ -118,7 +137,7 @@ fun ReaderPageActionsDialog( title = stringResource(MR.strings.action_share_second_page), icon = Icons.Outlined.Share, onClick = { - onShare(true) + onShare(false, true) onDismissRequest() }, ) @@ -140,7 +159,7 @@ fun ReaderPageActionsDialog( title = stringResource(MR.strings.action_share_combined_page), icon = Icons.Outlined.Share, onClick = { - onShareCombined() + onShareCombined(true) onDismissRequest() }, ) diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt index bda8aa4049..e3e3bebc7e 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt @@ -47,6 +47,7 @@ fun ReaderAppBars( bookmarked: Boolean, onToggleBookmarked: () -> Unit, onOpenInWebView: (() -> Unit)?, + onOpenInBrowser: (() -> Unit)?, onShare: (() -> Unit)?, viewer: Viewer?, @@ -56,7 +57,7 @@ fun ReaderAppBars( enabledPrevious: Boolean, currentPage: Int, totalPages: Int, - onSliderValueChange: (Int) -> Unit, + onPageIndexChange: (Int) -> Unit, readingMode: ReadingMode, onClickReadingMode: () -> Unit, @@ -136,6 +137,14 @@ fun ReaderAppBars( ), ) } + onOpenInBrowser?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_open_in_browser), + onClick = it, + ), + ) + } onShare?.let { add( AppBar.OverflowAction( @@ -176,10 +185,9 @@ fun ReaderAppBars( enabledPrevious = enabledPrevious, currentPage = currentPage, totalPages = totalPages, - onSliderValueChange = onSliderValueChange, + onPageIndexChange = onPageIndexChange, currentPageText = currentPageText, ) - BottomReaderBar( // SY --> enabledButtons = enabledButtons, diff --git a/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt index 46cc9aa4e7..cd82dc6012 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -16,26 +17,30 @@ import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.util.isTabletUi import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.Slider import tachiyomi.presentation.core.i18n.stringResource -import kotlin.math.roundToInt @Composable @Suppress("LongMethod") @@ -50,10 +55,10 @@ fun ChapterNavigator( currentPageText: String, // SY <-- totalPages: Int, - onSliderValueChange: (Int) -> Unit, + onPageIndexChange: (Int) -> Unit, ) { val isTabletUi = isTabletUi() - val horizontalPadding = if (isTabletUi) 24.dp else 16.dp + val horizontalPadding = if (isTabletUi) 24.dp else 8.dp val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr val haptic = LocalHapticFeedback.current @@ -98,8 +103,12 @@ fun ChapterNavigator( verticalAlignment = Alignment.CenterVertically, ) { // SY --> - Text(text = currentPageText) - // SY <-- + Box(contentAlignment = Alignment.CenterEnd) { + Text(text = currentPageText) + // SY <-- + // Taking up full length so the slider doesn't shift when 'currentPage' length changes + Text(text = totalPages.toString(), color = Color.Transparent) + } val interactionSource = remember { MutableInteractionSource() } val sliderDragged by interactionSource.collectIsDraggedAsState() @@ -113,11 +122,11 @@ fun ChapterNavigator( modifier = Modifier .weight(1f) .padding(horizontal = 8.dp), - value = currentPage.toFloat(), - valueRange = 1f..totalPages.toFloat(), - steps = totalPages - 2, - onValueChange = { - onSliderValueChange(it.roundToInt() - 1) + value = currentPage, + valueRange = 1..totalPages, + onValueChange = f@{ + if (it == currentPage) return@f + onPageIndexChange(it - 1) }, interactionSource = interactionSource, ) @@ -144,3 +153,22 @@ fun ChapterNavigator( } } } + +@Preview +@Composable +private fun ChapterNavigatorPreview() { + var currentPage by remember { mutableIntStateOf(1) } + TachiyomiPreviewTheme { + ChapterNavigator( + isRtl = false, + onNextChapter = {}, + enabledNext = true, + onPreviousChapter = {}, + enabledPrevious = true, + currentPage = currentPage, + currentPageText = currentPage.toString(), + totalPages = 10, + onPageIndexChange = { currentPage = (it + 1) }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHome.kt index 972f0ab65c..ee73e40272 100644 --- a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHome.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow @@ -47,8 +46,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import java.time.format.DateTimeFormatter -private const val UnsetStatusTextAlpha = 0.5F - @Composable fun AnimeTrackInfoDialogHome( trackItems: List, @@ -61,6 +58,7 @@ fun AnimeTrackInfoDialogHome( onNewSearch: (AnimeTrackItem) -> Unit, onOpenInBrowser: (AnimeTrackItem) -> Unit, onRemoved: (AnimeTrackItem) -> Unit, + onCopyLink: (AnimeTrackItem) -> Unit, ) { Column( modifier = Modifier @@ -109,6 +107,7 @@ fun AnimeTrackInfoDialogHome( onNewSearch = { onNewSearch(item) }, onOpenInBrowser = { onOpenInBrowser(item) }, onRemoved = { onRemoved(item) }, + onCopyLink = { onCopyLink(item) }, ) } else { TrackInfoItemEmpty( @@ -137,6 +136,7 @@ private fun TrackInfoItem( onNewSearch: () -> Unit, onOpenInBrowser: () -> Unit, onRemoved: () -> Unit, + onCopyLink: () -> Unit, ) { val context = LocalContext.current Column { @@ -146,6 +146,7 @@ private fun TrackInfoItem( TrackLogoIcon( tracker = tracker, onClick = onOpenInBrowser, + onLongClick = onCopyLink, ) Box( modifier = Modifier @@ -172,6 +173,7 @@ private fun TrackInfoItem( TrackInfoItemMenu( onOpenInBrowser = onOpenInBrowser, onRemoved = onRemoved, + onCopyLink = onCopyLink, ) } @@ -199,10 +201,9 @@ private fun TrackInfoItem( if (onScoreClick != null) { VerticalDivider() TrackDetailsItem( - modifier = Modifier - .weight(1f) - .alpha(if (score == null) UnsetStatusTextAlpha else 1f), - text = score ?: stringResource(MR.strings.score), + modifier = Modifier.weight(1f), + text = score, + placeholder = stringResource(MR.strings.score), onClick = onScoreClick, ) } diff --git a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHomePreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHomePreviewProvider.kt index 375829c5f0..ef907fb3cd 100644 --- a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHomePreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackInfoDialogHomePreviewProvider.kt @@ -56,6 +56,7 @@ internal class AnimeTrackInfoDialogHomePreviewProvider : onNewSearch = {}, onOpenInBrowser = {}, onRemoved = {}, + onCopyLink = {}, ) } @@ -71,6 +72,7 @@ internal class AnimeTrackInfoDialogHomePreviewProvider : onNewSearch = {}, onOpenInBrowser = {}, onRemoved = {}, + onCopyLink = {}, ) } diff --git a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearch.kt index 5f222aad83..9bd51ce01a 100644 --- a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearch.kt @@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.CheckCircle @@ -60,7 +62,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.toLowerCase @@ -85,8 +86,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable fun AnimeTrackerSearch( - query: TextFieldValue, - onQueryChange: (TextFieldValue) -> Unit, + state: TextFieldState, onDispatchQuery: () -> Unit, queryResult: Result>?, selected: AnimeTrackSearch?, @@ -116,22 +116,19 @@ fun AnimeTrackerSearch( }, title = { BasicTextField( - value = query, - onValueChange = onQueryChange, + state = state, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus), textStyle = MaterialTheme.typography.bodyLarge .copy(color = MaterialTheme.colorScheme.onSurface), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { dispatchQueryAndClearFocus() }, - ), + onKeyboardAction = { dispatchQueryAndClearFocus() }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { - if (query.text.isEmpty()) { + decorator = { + if (state.text.isEmpty()) { Text( text = stringResource(MR.strings.action_search_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -143,10 +140,10 @@ fun AnimeTrackerSearch( ) }, actions = { - if (query.text.isNotEmpty()) { + if (state.text.isNotEmpty()) { IconButton( onClick = { - onQueryChange(TextFieldValue()) + state.clearText() focusRequester.requestFocus() }, ) { @@ -227,6 +224,7 @@ private fun SearchResultItem( ) { val context = LocalContext.current val clipboardManager: ClipboardManager = LocalClipboardManager.current + val focusManager = LocalFocusManager.current val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current) val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current) val description = trackSearch.summary.trim() @@ -246,7 +244,10 @@ private fun SearchResultItem( ) .combinedClickable( onLongClick = { dropDownMenuExpanded = true }, - onClick = onClick, + onClick = { + focusManager.clearFocus() + onClick() + }, ) .padding(12.dp), ) { diff --git a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearchPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearchPreviewProvider.kt index 37b43b9ea3..967ca7dee2 100644 --- a/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearchPreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/anime/AnimeTrackerSearchPreviewProvider.kt @@ -1,7 +1,7 @@ package eu.kanade.presentation.track.anime +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch @@ -13,8 +13,7 @@ internal class AnimeTrackerSearchPreviewProvider : PreviewParameterProvider<@Com private val fullPageWithSecondSelected = @Composable { val items = someTrackSearches().take(30).toList() AnimeTrackerSearch( - query = TextFieldValue(text = "search text"), - onQueryChange = {}, + state = TextFieldState(initialText = "search text"), onDispatchQuery = {}, queryResult = Result.success(items), selected = items[1], @@ -25,8 +24,7 @@ internal class AnimeTrackerSearchPreviewProvider : PreviewParameterProvider<@Com } private val fullPageWithoutSelected = @Composable { AnimeTrackerSearch( - query = TextFieldValue(text = ""), - onQueryChange = {}, + state = TextFieldState(), onDispatchQuery = {}, queryResult = Result.success(someTrackSearches().take(30).toList()), selected = null, @@ -37,8 +35,7 @@ internal class AnimeTrackerSearchPreviewProvider : PreviewParameterProvider<@Com } private val loading = @Composable { AnimeTrackerSearch( - query = TextFieldValue(), - onQueryChange = {}, + state = TextFieldState(), onDispatchQuery = {}, queryResult = null, selected = null, diff --git a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt index 4dd03ecc71..7488d0da96 100644 --- a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt +++ b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt @@ -22,9 +22,10 @@ import tachiyomi.presentation.core.util.clickableNoIndication fun TrackLogoIcon( tracker: Tracker, onClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, ) { val modifier = if (onClick != null) { - Modifier.clickableNoIndication(onClick = onClick) + Modifier.clickableNoIndication(onClick = onClick, onLongClick = onLongClick) } else { Modifier } @@ -53,6 +54,7 @@ private fun TrackLogoIconPreviews( TrackLogoIcon( tracker = tracker, onClick = null, + onLongClick = null, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHome.kt index 7843458475..c749f5fb95 100644 --- a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHome.kt @@ -58,8 +58,6 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import java.time.format.DateTimeFormatter -private const val UnsetStatusTextAlpha = 0.5F - @Composable fun MangaTrackInfoDialogHome( trackItems: List, @@ -72,6 +70,7 @@ fun MangaTrackInfoDialogHome( onNewSearch: (MangaTrackItem) -> Unit, onOpenInBrowser: (MangaTrackItem) -> Unit, onRemoved: (MangaTrackItem) -> Unit, + onCopyLink: (MangaTrackItem) -> Unit, ) { Column( modifier = Modifier @@ -120,6 +119,7 @@ fun MangaTrackInfoDialogHome( onNewSearch = { onNewSearch(item) }, onOpenInBrowser = { onOpenInBrowser(item) }, onRemoved = { onRemoved(item) }, + onCopyLink = { onCopyLink(item) }, ) } else { TrackInfoItemEmpty( @@ -148,6 +148,7 @@ private fun TrackInfoItem( onNewSearch: () -> Unit, onOpenInBrowser: () -> Unit, onRemoved: () -> Unit, + onCopyLink: () -> Unit, ) { val context = LocalContext.current Column { @@ -157,6 +158,7 @@ private fun TrackInfoItem( TrackLogoIcon( tracker = tracker, onClick = onOpenInBrowser, + onLongClick = onCopyLink, ) Box( modifier = Modifier @@ -183,6 +185,7 @@ private fun TrackInfoItem( TrackInfoItemMenu( onOpenInBrowser = onOpenInBrowser, onRemoved = onRemoved, + onCopyLink = onCopyLink, ) } @@ -210,10 +213,9 @@ private fun TrackInfoItem( if (onScoreClick != null) { VerticalDivider() TrackDetailsItem( - modifier = Modifier - .weight(1f) - .alpha(if (score == null) UnsetStatusTextAlpha else 1f), - text = score ?: stringResource(MR.strings.score), + modifier = Modifier.weight(1f), + text = score, + placeholder = stringResource(MR.strings.score), onClick = onScoreClick, ) } @@ -242,6 +244,8 @@ private fun TrackInfoItem( } } +private const val UNSET_TEXT_ALPHA = 0.5F + @Composable fun TrackDetailsItem( text: String?, @@ -262,7 +266,7 @@ fun TrackDetailsItem( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UNSET_TEXT_ALPHA else 1f), ) } } @@ -291,6 +295,7 @@ private fun TrackInfoItemEmpty( fun TrackInfoItemMenu( onOpenInBrowser: () -> Unit, onRemoved: () -> Unit, + onCopyLink: () -> Unit, ) { var expanded by remember { mutableStateOf(false) } Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { @@ -311,6 +316,13 @@ fun TrackInfoItemMenu( expanded = false }, ) + DropdownMenuItem( + text = { Text(stringResource(MR.strings.action_copy_link)) }, + onClick = { + onCopyLink() + expanded = false + }, + ) DropdownMenuItem( text = { Text(stringResource(MR.strings.action_remove)) }, onClick = { diff --git a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHomePreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHomePreviewProvider.kt index eebcb9ea56..ef46f97275 100644 --- a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHomePreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackInfoDialogHomePreviewProvider.kt @@ -56,6 +56,7 @@ internal class MangaTrackInfoDialogHomePreviewProvider : onNewSearch = {}, onOpenInBrowser = {}, onRemoved = {}, + onCopyLink = {}, ) } @@ -71,6 +72,7 @@ internal class MangaTrackInfoDialogHomePreviewProvider : onNewSearch = {}, onOpenInBrowser = {}, onRemoved = {}, + onCopyLink = {}, ) } diff --git a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt index d05a2a011d..bce5841eb9 100644 --- a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearch.kt @@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.CheckCircle @@ -60,7 +62,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.toLowerCase @@ -85,8 +86,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable fun MangaTrackerSearch( - query: TextFieldValue, - onQueryChange: (TextFieldValue) -> Unit, + state: TextFieldState, onDispatchQuery: () -> Unit, queryResult: Result>?, selected: MangaTrackSearch?, @@ -116,22 +116,19 @@ fun MangaTrackerSearch( }, title = { BasicTextField( - value = query, - onValueChange = onQueryChange, + state = state, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus), textStyle = MaterialTheme.typography.bodyLarge .copy(color = MaterialTheme.colorScheme.onSurface), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { dispatchQueryAndClearFocus() }, - ), + onKeyboardAction = { dispatchQueryAndClearFocus() }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { - if (query.text.isEmpty()) { + decorator = { + if (state.text.isEmpty()) { Text( text = stringResource(MR.strings.action_search_hint), color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -143,10 +140,10 @@ fun MangaTrackerSearch( ) }, actions = { - if (query.text.isNotEmpty()) { + if (state.text.isNotEmpty()) { IconButton( onClick = { - onQueryChange(TextFieldValue()) + state.clearText() focusRequester.requestFocus() }, ) { @@ -227,6 +224,7 @@ private fun SearchResultItem( ) { val context = LocalContext.current val clipboardManager: ClipboardManager = LocalClipboardManager.current + val focusManager = LocalFocusManager.current val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current) val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current) val description = trackSearch.summary.trim() @@ -246,7 +244,10 @@ private fun SearchResultItem( ) .combinedClickable( onLongClick = { dropDownMenuExpanded = true }, - onClick = onClick, + onClick = { + focusManager.clearFocus() + onClick() + }, ) .padding(12.dp), ) { diff --git a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearchPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearchPreviewProvider.kt index e19a2f5cda..a320501c7b 100644 --- a/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearchPreviewProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/track/manga/MangaTrackerSearchPreviewProvider.kt @@ -1,7 +1,7 @@ package eu.kanade.presentation.track.manga +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch @@ -13,8 +13,7 @@ internal class MangaTrackerSearchPreviewProvider : PreviewParameterProvider<@Com private val fullPageWithSecondSelected = @Composable { val items = someTrackSearches().take(30).toList() MangaTrackerSearch( - query = TextFieldValue(text = "search text"), - onQueryChange = {}, + state = TextFieldState(initialText = "search text"), onDispatchQuery = {}, queryResult = Result.success(items), selected = items[1], @@ -25,8 +24,7 @@ internal class MangaTrackerSearchPreviewProvider : PreviewParameterProvider<@Com } private val fullPageWithoutSelected = @Composable { MangaTrackerSearch( - query = TextFieldValue(text = ""), - onQueryChange = {}, + state = TextFieldState(), onDispatchQuery = {}, queryResult = Result.success(someTrackSearches().take(30).toList()), selected = null, @@ -37,8 +35,7 @@ internal class MangaTrackerSearchPreviewProvider : PreviewParameterProvider<@Com } private val loading = @Composable { MangaTrackerSearch( - query = TextFieldValue(), - onQueryChange = {}, + state = TextFieldState(), onDispatchQuery = {}, queryResult = null, selected = null, diff --git a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt index 5173eb4e94..12b51e8ad5 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/anime/AnimeUpdatesUiItem.kt @@ -38,6 +38,7 @@ import eu.kanade.presentation.entries.anime.components.EpisodeDownloadAction import eu.kanade.presentation.entries.anime.components.EpisodeDownloadIndicator import eu.kanade.presentation.entries.components.DotSeparatorText import eu.kanade.presentation.entries.components.ItemCover +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadProvider import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload @@ -48,7 +49,7 @@ import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ListGroupHeader -import tachiyomi.presentation.core.components.material.ReadItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground @@ -61,7 +62,7 @@ internal fun LazyListScope.animeUpdatesLastUpdatedItem( item(key = "animeUpdates-lastUpdated") { Box( modifier = Modifier - .animateItem() + .animateItem(fadeInSpec = null, fadeOutSpec = null) .padding( horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small, @@ -101,14 +102,14 @@ internal fun LazyListScope.animeUpdatesUiItems( when (item) { is AnimeUpdatesUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), text = relativeDateText(item.date), ) } is AnimeUpdatesUiModel.Item -> { val updatesItem = item.item AnimeUpdatesUiItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), update = updatesItem.update, selected = updatesItem.selected, watchProgress = updatesItem.update.lastSecondSeen @@ -167,7 +168,7 @@ private fun AnimeUpdatesUiItem( modifier: Modifier = Modifier, ) { val haptic = LocalHapticFeedback.current - val textAlpha = if (update.seen) ReadItemAlpha else 1f + val textAlpha = if (update.seen) DISABLED_ALPHA else 1f Row( modifier = modifier @@ -242,7 +243,7 @@ private fun AnimeUpdatesUiItem( Text( text = watchProgress, maxLines = 1, - color = LocalContentColor.current.copy(alpha = ReadItemAlpha), + color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA), overflow = TextOverflow.Ellipsis, ) } @@ -271,7 +272,6 @@ private fun AnimeUpdatesUiItem( } // <-- AM (FILE_SIZE) - EpisodeDownloadIndicator( enabled = onDownloadEpisode != null, modifier = Modifier.padding(start = 4.dp), @@ -304,6 +304,7 @@ private fun formatProgress(milliseconds: Long): String { ) } } + // AM (FILE_SIZE) --> private val storagePreferences: StoragePreferences by injectLazy() private val animeDownloadProvider: AnimeDownloadProvider by injectLazy() diff --git a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt index 546af1f1e1..bbe4a932e1 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/manga/MangaUpdatesUiItem.kt @@ -37,13 +37,14 @@ import eu.kanade.presentation.entries.components.DotSeparatorText import eu.kanade.presentation.entries.components.ItemCover import eu.kanade.presentation.entries.manga.components.ChapterDownloadAction import eu.kanade.presentation.entries.manga.components.ChapterDownloadIndicator +import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.data.download.manga.model.MangaDownload import eu.kanade.tachiyomi.ui.updates.manga.MangaUpdatesItem import tachiyomi.domain.updates.manga.model.MangaUpdatesWithRelations import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.ListGroupHeader -import tachiyomi.presentation.core.components.material.ReadItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground @@ -54,7 +55,7 @@ internal fun LazyListScope.mangaUpdatesLastUpdatedItem( item(key = "mangaUpdates-lastUpdated") { Box( modifier = Modifier - .animateItem() + .animateItem(fadeInSpec = null, fadeOutSpec = null) .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), ) { Text( @@ -91,14 +92,14 @@ internal fun LazyListScope.mangaUpdatesUiItems( when (item) { is MangaUpdatesUiModel.Header -> { ListGroupHeader( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), text = relativeDateText(item.date), ) } is MangaUpdatesUiModel.Item -> { val updatesItem = item.item MangaUpdatesUiItem( - modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), update = updatesItem.update, selected = updatesItem.selected, readProgress = updatesItem.update.lastPageRead @@ -150,7 +151,7 @@ private fun MangaUpdatesUiItem( modifier: Modifier = Modifier, ) { val haptic = LocalHapticFeedback.current - val textAlpha = if (update.read) ReadItemAlpha else 1f + val textAlpha = if (update.read) DISABLED_ALPHA else 1f Row( modifier = modifier @@ -224,7 +225,7 @@ private fun MangaUpdatesUiItem( Text( text = readProgress, maxLines = 1, - color = LocalContentColor.current.copy(alpha = ReadItemAlpha), + color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA), overflow = TextOverflow.Ellipsis, ) } diff --git a/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt b/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt index 6d56c8e748..da278605bb 100644 --- a/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt +++ b/app/src/main/java/eu/kanade/presentation/util/ExceptionFormatter.kt @@ -1,7 +1,6 @@ package eu.kanade.presentation.util import android.content.Context -import eu.kanade.tachiyomi.animesource.online.LicensedEntryItemsException import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.util.system.isOnline import tachiyomi.core.common.i18n.stringResource @@ -30,9 +29,6 @@ val Throwable.formattedMessage: String is SourceNotInstalledException, is AnimeSourceNotInstalledException -> return stringResource( MR.strings.loader_not_implemented_error, ) - is LicensedEntryItemsException -> return stringResource( - MR.strings.licensed_manga_chapters_error, - ) } return when (val className = this::class.simpleName) { "Exception", "IOException" -> message ?: className diff --git a/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt b/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt new file mode 100644 index 0000000000..a6c9f70198 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/FastScrollAnimateItem.kt @@ -0,0 +1,8 @@ +package eu.kanade.presentation.util + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.ui.Modifier + +// https://issuetracker.google.com/352584409 +context(LazyItemScope) +fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null) diff --git a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt index 0cb117cc74..734dd75f68 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Navigator.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Navigator.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.util -import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform @@ -33,15 +32,12 @@ import uy.kohesive.injekt.api.get /** * For invoking back press to the parent activity */ -@SuppressLint("ComposeCompositionLocalUsage") val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } private val uiPreferences: UiPreferences = Injekt.get() -abstract class Tab : cafe.adriel.voyager.navigator.tab.Tab { - - override val key: ScreenKey = uniqueScreenKey - open suspend fun onReselect(navigator: Navigator) {} +interface Tab : cafe.adriel.voyager.navigator.tab.Tab { + suspend fun onReselect(navigator: Navigator) {} @Composable fun currentNavigationStyle(): NavStyle = uiPreferences.navStyle().collectAsState().value @@ -69,7 +65,10 @@ interface AssistContentScreen { } @Composable -fun DefaultNavigatorScreenTransition(navigator: Navigator) { +fun DefaultNavigatorScreenTransition( + navigator: Navigator, + modifier: Modifier = Modifier, +) { val slideDistance = rememberSlideDistance() ScreenTransition( navigator = navigator, @@ -79,6 +78,7 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) { slideDistance = slideDistance, ) }, + modifier = modifier, ) } diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index d12c6647bb..33a827db1f 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -99,6 +99,17 @@ fun WebViewScreenContent( request: WebResourceRequest?, ): Boolean { request?.let { + // Don't attempt to open blobs as webpages + if (it.url.toString().startsWith("blob:http")) { + return false + } + + // Ignore intents urls + if (it.url.toString().startsWith("intent://")) { + return true + } + + // Continue with request, but with custom headers view?.loadUrl(it.url.toString(), headers) } return super.shouldOverrideUrlLoading(view, request) diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 6e35610d51..1cdbb88138 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -178,7 +178,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor ) } - @Suppress("MagicNumber") override fun newImageLoader(context: Context): ImageLoader { return ImageLoader.Builder(this).apply { val callFactoryLazy = lazy { Injekt.get().client } diff --git a/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt index 600dac444d..cb08371b2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/crash/GlobalExceptionHandler.kt @@ -11,7 +11,6 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import logcat.LogPriority import tachiyomi.core.common.util.system.logcat -import kotlin.system.exitProcess class GlobalExceptionHandler private constructor( private val applicationContext: Context, @@ -31,13 +30,9 @@ class GlobalExceptionHandler private constructor( } override fun uncaughtException(thread: Thread, exception: Throwable) { - try { - logcat(priority = LogPriority.ERROR, throwable = exception) - launchActivity(applicationContext, activityToBeLaunched, exception) - exitProcess(0) - } catch (_: Exception) { - defaultHandler.uncaughtException(thread, exception) - } + logcat(priority = LogPriority.ERROR, throwable = exception) + launchActivity(applicationContext, activityToBeLaunched, exception) + defaultHandler.uncaughtException(thread, exception) } private fun launchActivity( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt index 34f862548f..55553082fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt @@ -3,18 +3,21 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.data.backup.models.Backup +import kotlinx.serialization.SerializationException import kotlinx.serialization.protobuf.ProtoBuf import okio.buffer import okio.gzip import okio.source +import tachiyomi.core.common.i18n.stringResource +import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.io.IOException class BackupDecoder( private val context: Context, private val parser: ProtoBuf = Injekt.get(), ) { - /** * Decode a potentially-gzipped backup. */ @@ -26,13 +29,25 @@ class BackupDecoder( require(2) } val id1id2 = peeked.readShort() - val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes - source.gzip().buffer() - } else { - source + val backupString = when (id1id2.toInt()) { + 0x1f8b -> source.gzip().buffer() // 0x1f8b is gzip magic bytes + MAGIC_JSON_SIGNATURE1, MAGIC_JSON_SIGNATURE2, MAGIC_JSON_SIGNATURE3 -> { + throw IOException(context.stringResource(MR.strings.invalid_backup_file_json)) + } + else -> source }.use { it.readByteArray() } - parser.decodeFromByteArray(Backup.serializer(), backupString) + try { + parser.decodeFromByteArray(Backup.serializer(), backupString) + } catch (_: SerializationException) { + throw IOException(context.stringResource(MR.strings.invalid_backup_file_unknown)) + } } } + + companion object { + private const val MAGIC_JSON_SIGNATURE1 = 0x7b7d // `{}` + private const val MAGIC_JSON_SIGNATURE2 = 0x7b22 // `{"` + private const val MAGIC_JSON_SIGNATURE3 = 0x7b0a // `{\n` + } } 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 3afe0ee9e9..fe51eac511 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,16 +6,21 @@ 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.AnimeBackupCreator -import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.AnimeCategoriesBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.AnimeExtensionRepoBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.AnimeSourcesBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionsBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.MangaCategoriesBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.MangaExtensionRepoBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.MangaSourcesBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator -import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupAnime import eu.kanade.tachiyomi.data.backup.models.BackupAnimeSource import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupExtension +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSource @@ -30,8 +35,10 @@ import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites import tachiyomi.domain.entries.anime.model.Anime +import tachiyomi.domain.entries.anime.repository.AnimeRepository import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites import tachiyomi.domain.entries.manga.model.Manga +import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -49,51 +56,66 @@ class BackupCreator( private val getAnimeFavorites: GetAnimeFavorites = Injekt.get(), private val getMangaFavorites: GetMangaFavorites = Injekt.get(), private val backupPreferences: BackupPreferences = Injekt.get(), + private val mangaRepository: MangaRepository = Injekt.get(), + private val animeRepository: AnimeRepository = Injekt.get(), - private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), + private val animeCategoriesBackupCreator: AnimeCategoriesBackupCreator = AnimeCategoriesBackupCreator(), + private val mangaCategoriesBackupCreator: MangaCategoriesBackupCreator = MangaCategoriesBackupCreator(), private val animeBackupCreator: AnimeBackupCreator = AnimeBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(), - private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(), + private val animeExtensionRepoBackupCreator: AnimeExtensionRepoBackupCreator = AnimeExtensionRepoBackupCreator(), + private val mangaExtensionRepoBackupCreator: MangaExtensionRepoBackupCreator = MangaExtensionRepoBackupCreator(), + private val animeSourcesBackupCreator: AnimeSourcesBackupCreator = AnimeSourcesBackupCreator(), + private val mangaSourcesBackupCreator: MangaSourcesBackupCreator = MangaSourcesBackupCreator(), private val extensionsBackupCreator: ExtensionsBackupCreator = ExtensionsBackupCreator(context), ) { suspend fun backup(uri: Uri, options: BackupOptions): String { var file: UniFile? = null try { - file = ( - if (isAutoBackup) { - // Get dir of file and create - val dir = UniFile.fromUri(context, uri) - - // Delete older backups - dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) } - .orEmpty() - .sortedByDescending { it.name } - .drop(MAX_AUTO_BACKUPS - 1) - .forEach { it.delete() } - - // Create new file to place backup - dir?.createFile(getFilename()) - } else { - UniFile.fromUri(context, uri) - } - ) + file = if (isAutoBackup) { + // Get dir of file and create + val dir = UniFile.fromUri(context, uri) + // Delete older backups + dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) } + .orEmpty() + .sortedByDescending { it.name } + .drop(MAX_AUTO_BACKUPS - 1) + .forEach { it.delete() } + // Create new file to place backup + dir?.createFile(getFilename()) + } else { + UniFile.fromUri(context, uri) + } if (file == null || !file.isFile) { throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error)) } - val databaseAnime = getAnimeFavorites.await() - val databaseManga = getMangaFavorites.await() + val nonFavoriteAnime = if (options.readEntries) { + animeRepository.getWatchedAnimeNotInLibrary() + } else { + emptyList() + } + val backupAnime = backupAnimes(getAnimeFavorites.await() + nonFavoriteAnime, options) + val nonFavoriteManga = if (options.readEntries) { + mangaRepository.getReadMangaNotInLibrary() + } else { + emptyList() + } + val backupManga = backupMangas(getMangaFavorites.await() + nonFavoriteManga, options) + val backup = Backup( - backupManga = backupMangas(databaseManga, options), + backupManga = backupManga, backupCategories = backupMangaCategories(options), - backupAnime = backupAnimes(databaseAnime, options), + backupAnime = backupAnime, backupAnimeCategories = backupAnimeCategories(options), - backupSources = backupMangaSources(databaseManga), - backupAnimeSources = backupAnimeSources(databaseAnime), + backupSources = backupMangaSources(backupManga), + backupAnimeSources = backupAnimeSources(backupAnime), backupPreferences = backupAppPreferences(options), + backupAnimeExtensionRepo = backupAnimeExtensionRepos(options), + backupMangaExtensionRepo = backupMangaExtensionRepos(options), backupSourcePreferences = backupSourcePreferences(options), backupExtensions = backupExtensions(options), ) @@ -131,46 +153,62 @@ class BackupCreator( suspend fun backupAnimeCategories(options: BackupOptions): List { if (!options.categories) return emptyList() - return categoriesBackupCreator.backupAnimeCategories() + return animeCategoriesBackupCreator() } suspend fun backupMangaCategories(options: BackupOptions): List { if (!options.categories) return emptyList() - return categoriesBackupCreator.backupMangaCategories() + return mangaCategoriesBackupCreator() } suspend fun backupMangas(mangas: List, options: BackupOptions): List { - return mangaBackupCreator.backupMangas(mangas, options) + if (!options.libraryEntries) return emptyList() + + return mangaBackupCreator(mangas, options) } suspend fun backupAnimes(animes: List, options: BackupOptions): List { - return animeBackupCreator.backupAnimes(animes, options) + if (!options.libraryEntries) return emptyList() + + return animeBackupCreator(animes, options) } - fun backupAnimeSources(animes: List): List { - return sourcesBackupCreator.backupAnimeSources(animes) + fun backupAnimeSources(animes: List): List { + return animeSourcesBackupCreator(animes) } - fun backupMangaSources(mangas: List): List { - return sourcesBackupCreator.backupMangaSources(mangas) + fun backupMangaSources(mangas: List): List { + return mangaSourcesBackupCreator(mangas) } fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() - return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings) + return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings) + } + + private suspend fun backupAnimeExtensionRepos(options: BackupOptions): List { + if (!options.extensionRepoSettings) return emptyList() + + return animeExtensionRepoBackupCreator() + } + + private suspend fun backupMangaExtensionRepos(options: BackupOptions): List { + if (!options.extensionRepoSettings) return emptyList() + + return mangaExtensionRepoBackupCreator() } fun backupSourcePreferences(options: BackupOptions): List { if (!options.sourceSettings) return emptyList() - return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings) + return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings) } private fun backupExtensions(options: BackupOptions): List { if (!options.extensions) return emptyList() - return extensionsBackupCreator.backupExtensions() + return extensionsBackupCreator() } companion object { 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 b1a2ebf066..c86237d86e 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 @@ -10,7 +10,9 @@ data class BackupOptions( val chapters: Boolean = true, val tracking: Boolean = true, val history: Boolean = true, + val readEntries: Boolean = true, val appSettings: Boolean = true, + val extensionRepoSettings: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, val extensions: Boolean = false, @@ -22,13 +24,15 @@ data class BackupOptions( chapters, tracking, history, + readEntries, appSettings, + extensionRepoSettings, sourceSettings, privateSettings, extensions, ) - fun anyEnabled() = libraryEntries || appSettings || sourceSettings + fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings companion object { val libraryOptions = persistentListOf( @@ -37,12 +41,6 @@ data class BackupOptions( getter = BackupOptions::libraryEntries, setter = { options, enabled -> options.copy(libraryEntries = enabled) }, ), - Entry( - label = MR.strings.categories, - getter = BackupOptions::categories, - setter = { options, enabled -> options.copy(categories = enabled) }, - enabled = { it.libraryEntries }, - ), Entry( label = MR.strings.chapters_episodes, getter = BackupOptions::chapters, @@ -61,6 +59,17 @@ data class BackupOptions( setter = { options, enabled -> options.copy(history = enabled) }, enabled = { it.libraryEntries }, ), + Entry( + label = MR.strings.categories, + getter = BackupOptions::categories, + setter = { options, enabled -> options.copy(categories = enabled) }, + ), + Entry( + label = MR.strings.non_library_settings, + getter = BackupOptions::readEntries, + setter = { options, enabled -> options.copy(readEntries = enabled) }, + enabled = { it.libraryEntries }, + ), ) val settingsOptions = persistentListOf( @@ -69,6 +78,11 @@ data class BackupOptions( getter = BackupOptions::appSettings, setter = { options, enabled -> options.copy(appSettings = enabled) }, ), + Entry( + label = MR.strings.extensionRepo_settings, + getter = BackupOptions::extensionRepoSettings, + setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + ), Entry( label = MR.strings.source_settings, getter = BackupOptions::sourceSettings, @@ -96,10 +110,12 @@ data class BackupOptions( chapters = array[2], tracking = array[3], history = array[4], - appSettings = array[5], - sourceSettings = array[6], - privateSettings = array[7], - extensions = array[8], + readEntries = array[5], + appSettings = array[6], + extensionRepoSettings = array[7], + sourceSettings = array[8], + privateSettings = array[9], + extensions = array[10], ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeBackupCreator.kt index 196173c76b..1a46e29f36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeBackupCreator.kt @@ -19,7 +19,7 @@ class AnimeBackupCreator( private val getHistory: GetAnimeHistory = Injekt.get(), ) { - suspend fun backupAnimes(animes: List, options: BackupOptions): List { + suspend operator fun invoke(animes: List, options: BackupOptions): List { return animes.map { backupAnime(it, options) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeCategoriesBackupCreator.kt similarity index 57% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeCategoriesBackupCreator.kt index 6c4507c7e2..b9d1dfcf73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeCategoriesBackupCreator.kt @@ -3,25 +3,17 @@ package eu.kanade.tachiyomi.data.backup.create.creators import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper import tachiyomi.domain.category.anime.interactor.GetAnimeCategories -import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.model.Category import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class CategoriesBackupCreator( +class AnimeCategoriesBackupCreator( private val getAnimeCategories: GetAnimeCategories = Injekt.get(), - private val getMangaCategories: GetMangaCategories = Injekt.get(), ) { - suspend fun backupAnimeCategories(): List { + suspend operator fun invoke(): List { return getAnimeCategories.await() .filterNot(Category::isSystemCategory) .map(backupCategoryMapper) } - - suspend fun backupMangaCategories(): List { - return getMangaCategories.await() - .filterNot(Category::isSystemCategory) - .map(backupCategoryMapper) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeExtensionRepoBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeExtensionRepoBackupCreator.kt new file mode 100644 index 0000000000..dafe2b6cf5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeExtensionRepoBackupCreator.kt @@ -0,0 +1,17 @@ +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.anime.interactor.GetAnimeExtensionRepo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class AnimeExtensionRepoBackupCreator( + private val getAnimeExtensionRepos: GetAnimeExtensionRepo = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getAnimeExtensionRepos.getAll() + .map(backupExtensionReposMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeSourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeSourcesBackupCreator.kt new file mode 100644 index 0000000000..bd90eea3a7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/AnimeSourcesBackupCreator.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.animesource.AnimeSource +import eu.kanade.tachiyomi.data.backup.models.BackupAnime +import eu.kanade.tachiyomi.data.backup.models.BackupAnimeSource +import tachiyomi.domain.source.anime.service.AnimeSourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class AnimeSourcesBackupCreator( + private val animeSourceManager: AnimeSourceManager = Injekt.get(), +) { + + operator fun invoke(animes: List): List { + return animes + .asSequence() + .map(BackupAnime::source) + .distinct() + .map(animeSourceManager::getOrStub) + .map { it.toBackupSource() } + .toList() + } +} + +private fun AnimeSource.toBackupSource() = + BackupAnimeSource( + name = this.name, + sourceId = this.id, + ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionsBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionsBackupCreator.kt index 325ff173d0..58b729f4d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionsBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionsBackupCreator.kt @@ -15,7 +15,7 @@ class ExtensionsBackupCreator( private val mangaExtensionManager: MangaExtensionManager = Injekt.get(), ) { - fun backupExtensions(): List { + operator fun invoke(): List { val installedExtensions = mutableListOf() animeExtensionManager.installedExtensionsFlow.value.forEach { val packageName = it.pkgName diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index f8a9743307..50de6747ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -20,7 +20,7 @@ class MangaBackupCreator( private val getHistory: GetMangaHistory = Injekt.get(), ) { - suspend fun backupMangas(mangas: List, options: BackupOptions): List { + suspend operator fun invoke(mangas: List, options: BackupOptions): List { return mangas.map { backupManga(it, options) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaCategoriesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaCategoriesBackupCreator.kt new file mode 100644 index 0000000000..2a06d452eb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaCategoriesBackupCreator.kt @@ -0,0 +1,19 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper +import tachiyomi.domain.category.manga.interactor.GetMangaCategories +import tachiyomi.domain.category.model.Category +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaCategoriesBackupCreator( + private val getMangaCategories: GetMangaCategories = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getMangaCategories.await() + .filterNot(Category::isSystemCategory) + .map(backupCategoryMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaExtensionRepoBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaExtensionRepoBackupCreator.kt new file mode 100644 index 0000000000..6a387d19fa --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaExtensionRepoBackupCreator.kt @@ -0,0 +1,17 @@ +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.manga.interactor.GetMangaExtensionRepo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaExtensionRepoBackupCreator( + private val getMangaExtensionRepos: GetMangaExtensionRepo = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getMangaExtensionRepos.getAll() + .map(backupExtensionReposMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaSourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaSourcesBackupCreator.kt new file mode 100644 index 0000000000..7f5be0bfc9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaSourcesBackupCreator.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupSource +import eu.kanade.tachiyomi.source.MangaSource +import tachiyomi.domain.source.manga.service.MangaSourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaSourcesBackupCreator( + private val mangaSourceManager: MangaSourceManager = Injekt.get(), +) { + + operator fun invoke(mangas: List): List { + return mangas + .asSequence() + .map(BackupManga::source) + .distinct() + .map(mangaSourceManager::getOrStub) + .map { it.toBackupSource() } + .toList() + } +} + +private fun MangaSource.toBackupSource() = + BackupSource( + name = this.name, + sourceId = this.id, + ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index 77b5c5798f..5df4124ffb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -27,12 +27,12 @@ class PreferenceBackupCreator( private val preferenceStore: PreferenceStore = Injekt.get(), ) { - fun backupAppPreferences(includePrivatePreferences: Boolean): List { + fun createApp(includePrivatePreferences: Boolean): List { return preferenceStore.getAll().toBackupPreferences() .withPrivatePreferences(includePrivatePreferences) } - fun backupSourcePreferences(includePrivatePreferences: Boolean): List { + fun createSource(includePrivatePreferences: Boolean): List { val animePreferences = animeSourceManager.getCatalogueSources() .filterIsInstance() .map { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt deleted file mode 100644 index 4be3159421..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt +++ /dev/null @@ -1,50 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.create.creators - -import eu.kanade.tachiyomi.animesource.AnimeSource -import eu.kanade.tachiyomi.data.backup.models.BackupAnimeSource -import eu.kanade.tachiyomi.data.backup.models.BackupSource -import eu.kanade.tachiyomi.source.MangaSource -import tachiyomi.domain.entries.anime.model.Anime -import tachiyomi.domain.entries.manga.model.Manga -import tachiyomi.domain.source.anime.service.AnimeSourceManager -import tachiyomi.domain.source.manga.service.MangaSourceManager -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SourcesBackupCreator( - private val animeSourceManager: AnimeSourceManager = Injekt.get(), - private val mangaSourceManager: MangaSourceManager = Injekt.get(), -) { - - fun backupAnimeSources(animes: List): List { - return animes - .asSequence() - .map(Anime::source) - .distinct() - .map(animeSourceManager::getOrStub) - .map { it.toBackupSource() } - .toList() - } - - fun backupMangaSources(mangas: List): List { - return mangas - .asSequence() - .map(Manga::source) - .distinct() - .map(mangaSourceManager::getOrStub) - .map { it.toBackupSource() } - .toList() - } -} - -private fun AnimeSource.toBackupSource() = - BackupAnimeSource( - name = this.name, - sourceId = this.id, - ) - -private fun MangaSource.toBackupSource() = - BackupSource( - name = this.name, - sourceId = this.id, - ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt index c39613464e..4a0e2d33b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt @@ -5,8 +5,6 @@ import eu.kanade.tachiyomi.data.backup.models.BackupAnimeSource import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupSource -import eu.kanade.tachiyomi.data.backup.models.BrokenBackupAnimeSource -import eu.kanade.tachiyomi.data.backup.models.BrokenBackupSource import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @@ -17,9 +15,9 @@ data class Backup( @ProtoNumber(3) val backupAnime: List = emptyList(), @ProtoNumber(4) var backupAnimeCategories: List = emptyList(), // Bump by 100 to specify this is a 0.x value - @ProtoNumber(100) var backupBrokenSources: List = emptyList(), + // @ProtoNumber(100) var backupBrokenSources, legacy source model with non-compliant proto number, @ProtoNumber(101) var backupSources: List = emptyList(), - @ProtoNumber(102) var backupBrokenAnimeSources: List = emptyList(), + // @ProtoNumber(102) var backupBrokenAnimeSources, legacy source model with non-compliant proto number, @ProtoNumber(103) var backupAnimeSources: List = emptyList(), @ProtoNumber(104) var backupPreferences: List = emptyList(), ) 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 67ea342fa0..bb193e85e4 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 @@ -10,11 +10,13 @@ data class Backup( @ProtoNumber(3) val backupAnime: List = emptyList(), @ProtoNumber(4) var backupAnimeCategories: List = emptyList(), // Bump by 100 to specify this is a 0.x value - @ProtoNumber(100) var backupBrokenSources: List = emptyList(), + // @ProtoNumber(100) var backupBrokenSources, legacy source model with non-compliant proto number, @ProtoNumber(101) var backupSources: List = emptyList(), - @ProtoNumber(102) var backupBrokenAnimeSources: List = emptyList(), + // @ProtoNumber(102) var backupBrokenAnimeSources, legacy source model with non-compliant proto number, @ProtoNumber(103) var backupAnimeSources: List = emptyList(), @ProtoNumber(104) var backupPreferences: List = emptyList(), @ProtoNumber(105) var backupSourcePreferences: List = emptyList(), @ProtoNumber(106) var backupExtensions: List = emptyList(), + @ProtoNumber(107) var backupAnimeExtensionRepo: List = emptyList(), + @ProtoNumber(108) var backupMangaExtensionRepo: List = emptyList(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnime.kt index c54b8ec2c6..aa4ac4d834 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnime.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnime.kt @@ -7,7 +7,7 @@ import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.CustomAnimeInfo @Suppress( - + "DEPRECATION", "MagicNumber", ) @@ -36,7 +36,7 @@ data class BackupAnime( // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x @ProtoNumber(100) var favorite: Boolean = true, @ProtoNumber(101) var episodeFlags: Int = 0, - @ProtoNumber(102) var brokenHistory: List = emptyList(), + // @ProtoNumber(102) var brokenHistory, legacy history model with non-compliant proto number @ProtoNumber(103) var viewer_flags: Int = 0, @ProtoNumber(104) var history: List = emptyList(), @ProtoNumber(105) var updateStrategy: AnimeUpdateStrategy = AnimeUpdateStrategy.ALWAYS_UPDATE, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeHistory.kt index a67e90c75f..4b04fc76e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeHistory.kt @@ -17,14 +17,3 @@ data class BackupAnimeHistory( ) } } - -@Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.") -@Serializable -data class BrokenBackupAnimeHistory( - @ProtoNumber(0) var url: String, - @ProtoNumber(1) var lastSeen: Long, -) { - fun toBackupHistory(): BackupAnimeHistory { - return BackupAnimeHistory(url, lastSeen) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt index e3cf8e85f1..be64e8759d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupAnimeTracking.kt @@ -74,8 +74,7 @@ data class BackupAnimeTracking( val backupAnimeTrackMapper = { _id: Long, - anime_id: - Long, + anime_id: Long, syncId: Long, mediaId: Long, libraryId: Long?, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index ef61e091d1..3c98d3ac48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.items.chapter.model.Chapter -@Suppress("MagicNumber") @Serializable data class BackupChapter( // in 1.x some of these values have different names diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupEpisode.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupEpisode.kt index 0b09569bac..9ade946ab7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupEpisode.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupEpisode.kt @@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.items.episode.model.Episode -@Suppress("MagicNumber") @Serializable data class BackupEpisode( // in 1.x some of these values have different names 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 new file mode 100644 index 0000000000..256def7b55 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt @@ -0,0 +1,24 @@ +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/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index 5238bcc841..46d65f368b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt @@ -18,15 +18,3 @@ data class BackupHistory( ) } } - -@Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.") -@Serializable -data class BrokenBackupHistory( - @ProtoNumber(0) var url: String, - @ProtoNumber(1) var lastRead: Long, - @ProtoNumber(2) var readDuration: Long = 0, -) { - fun toBackupHistory(): BackupHistory { - return BackupHistory(url, lastRead, readDuration) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 38ee09e2ad..f568713b67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -7,7 +7,7 @@ import tachiyomi.domain.entries.manga.model.CustomMangaInfo import tachiyomi.domain.entries.manga.model.Manga @Suppress( - + "DEPRECATION", "MagicNumber", ) @@ -37,7 +37,7 @@ data class BackupManga( // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x @ProtoNumber(100) var favorite: Boolean = true, @ProtoNumber(101) var chapterFlags: Int = 0, - @ProtoNumber(102) var brokenHistory: List = emptyList(), + // @ProtoNumber(102) var brokenHistory, legacy history model with non-compliant proto number @ProtoNumber(103) var viewer_flags: Int? = null, @ProtoNumber(104) var history: List = emptyList(), @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, 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 29d3f684bb..faf175bd91 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 @@ -7,12 +7,16 @@ import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.models.BackupAnime import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupExtension +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.data.backup.restore.restorers.AnimeCategoriesRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.AnimeExtensionRepoRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.AnimeRestorer -import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionsRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaCategoriesRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaExtensionRepoRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer import eu.kanade.tachiyomi.util.system.createFileInCacheDir @@ -32,8 +36,11 @@ class BackupRestorer( private val notifier: BackupNotifier, private val isSync: Boolean, - private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), + private val animeCategoriesRestorer: AnimeCategoriesRestorer = AnimeCategoriesRestorer(), + private val mangaCategoriesRestorer: MangaCategoriesRestorer = MangaCategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), + private val animeExtensionRepoRestorer: AnimeExtensionRepoRestorer = AnimeExtensionRepoRestorer(), + private val mangaExtensionRepoRestorer: MangaExtensionRepoRestorer = MangaExtensionRepoRestorer(), private val animeRestorer: AnimeRestorer = AnimeRestorer(isSync), private val mangaRestorer: MangaRestorer = MangaRestorer(isSync), private val extensionsRestorer: ExtensionsRestorer = ExtensionsRestorer(context), @@ -71,17 +78,23 @@ class BackupRestorer( val backup = BackupDecoder(context).decode(uri) // Store source mapping for error messages - val backupAnimeMaps = backup.backupAnimeSources + backup.backupBrokenAnimeSources.map { it.toBackupSource() } + val backupAnimeMaps = backup.backupAnimeSources animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name } - val backupMangaMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() } + val backupMangaMaps = backup.backupSources mangaSourceMapping = backupMangaMaps.associate { it.sourceId to it.name } - if (options.library) { - restoreAmount += backup.backupManga.size + backup.backupAnime.size + 2 // +2 for anime and manga categories + if (options.libraryEntries) { + restoreAmount += backup.backupManga.size + backup.backupAnime.size + } + if (options.categories) { + restoreAmount += 2 // +2 for anime and manga categories } if (options.appSettings) { restoreAmount += 1 } + if (options.extensionRepoSettings) { + restoreAmount += backup.backupAnimeExtensionRepo.size + backup.backupMangaExtensionRepo.size + } if (options.sourceSettings) { restoreAmount += 1 } @@ -90,7 +103,7 @@ class BackupRestorer( } coroutineScope { - if (options.library) { + if (options.categories) { restoreCategories( backupAnimeCategories = backup.backupAnimeCategories, backupMangaCategories = backup.backupCategories, @@ -102,9 +115,12 @@ class BackupRestorer( if (options.sourceSettings) { restoreSourcePreferences(backup.backupSourcePreferences) } - if (options.library) { - restoreAnime(backup.backupAnime, backup.backupAnimeCategories) - restoreManga(backup.backupManga, backup.backupCategories) + if (options.libraryEntries) { + restoreAnime(backup.backupAnime, if (options.categories) backup.backupAnimeCategories else emptyList()) + restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList()) + } + if (options.extensionRepoSettings) { + restoreExtensionRepos(backup.backupAnimeExtensionRepo, backup.backupMangaExtensionRepo) } if (options.extensions) { restoreExtensions(backup.backupExtensions) @@ -119,8 +135,8 @@ class BackupRestorer( backupMangaCategories: List, ) = launch { ensureActive() - categoriesRestorer.restoreAnimeCategories(backupAnimeCategories) - categoriesRestorer.restoreMangaCategories(backupMangaCategories) + animeCategoriesRestorer(backupAnimeCategories) + mangaCategoriesRestorer(backupMangaCategories) restoreProgress += 1 notifier.showRestoreProgress( @@ -140,7 +156,7 @@ class BackupRestorer( ensureActive() try { - animeRestorer.restoreAnime(it, backupAnimeCategories) + animeRestorer.restore(it, backupAnimeCategories) } catch (e: Exception) { val sourceName = animeSourceMapping[it.source] ?: it.source.toString() errors.add(Date() to "${it.title} [$sourceName]: ${e.message}") @@ -160,7 +176,7 @@ class BackupRestorer( ensureActive() try { - mangaRestorer.restoreManga(it, backupMangaCategories) + mangaRestorer.restore(it, backupMangaCategories) } catch (e: Exception) { val sourceName = mangaSourceMapping[it.source] ?: it.source.toString() errors.add(Date() to "${it.title} [$sourceName]: ${e.message}") @@ -173,7 +189,7 @@ class BackupRestorer( private fun CoroutineScope.restoreAppPreferences(preferences: List) = launch { ensureActive() - preferenceRestorer.restoreAppPreferences(preferences) + preferenceRestorer.restoreApp(preferences) restoreProgress += 1 notifier.showRestoreProgress( @@ -186,7 +202,7 @@ class BackupRestorer( private fun CoroutineScope.restoreSourcePreferences(preferences: List) = launch { ensureActive() - preferenceRestorer.restoreSourcePreferences(preferences) + preferenceRestorer.restoreSource(preferences) restoreProgress += 1 notifier.showRestoreProgress( @@ -197,6 +213,49 @@ class BackupRestorer( ) } + private fun CoroutineScope.restoreExtensionRepos( + backupAnimeExtensionRepo: List, + backupMangaExtensionRepo: List, + ) = launch { + backupAnimeExtensionRepo + .forEach { + ensureActive() + + try { + animeExtensionRepoRestorer(it) + } catch (e: Exception) { + errors.add(Date() to "Error Adding Anime Repo: ${it.name} : ${e.message}") + } + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.extensionRepo_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + backupMangaExtensionRepo + .forEach { + ensureActive() + + try { + mangaExtensionRepoRestorer(it) + } catch (e: Exception) { + errors.add(Date() to "Error Adding Manga Repo: ${it.name} : ${e.message}") + } + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.extensionRepo_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + } + private fun CoroutineScope.restoreExtensions(extensions: List) = launch { ensureActive() extensionsRestorer.restoreExtensions(extensions) @@ -213,7 +272,7 @@ class BackupRestorer( private fun writeErrorLog(): File { try { if (errors.isNotEmpty()) { - val file = context.createFileInCacheDir("tachiyomi_restore.txt") + val file = context.createFileInCacheDir("aniyomi_restore_error.txt") val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) file.bufferedWriter().use { out -> 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 75b2ec0712..48154ae6d8 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 @@ -5,33 +5,48 @@ import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR data class RestoreOptions( - val library: Boolean = true, + val libraryEntries: Boolean = true, + val categories: Boolean = true, val appSettings: Boolean = true, + val extensionRepoSettings: Boolean = true, val sourceSettings: Boolean = true, val extensions: Boolean = false, ) { fun asBooleanArray() = booleanArrayOf( - library, + libraryEntries, + categories, appSettings, + extensionRepoSettings, sourceSettings, extensions, ) - fun anyEnabled() = library || appSettings || sourceSettings || extensions + fun canRestore() = + libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || extensions companion object { val options = persistentListOf( Entry( label = MR.strings.label_library, - getter = RestoreOptions::library, - setter = { options, enabled -> options.copy(library = enabled) }, + getter = RestoreOptions::libraryEntries, + setter = { options, enabled -> options.copy(libraryEntries = enabled) }, + ), + Entry( + label = MR.strings.categories, + getter = RestoreOptions::categories, + setter = { options, enabled -> options.copy(categories = enabled) }, ), Entry( label = MR.strings.app_settings, getter = RestoreOptions::appSettings, setter = { options, enabled -> options.copy(appSettings = enabled) }, ), + Entry( + label = MR.strings.extensionRepo_settings, + getter = RestoreOptions::extensionRepoSettings, + setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + ), Entry( label = MR.strings.source_settings, getter = RestoreOptions::sourceSettings, @@ -45,10 +60,12 @@ data class RestoreOptions( ) fun fromBooleanArray(array: BooleanArray) = RestoreOptions( - library = array[0], - appSettings = array[1], - sourceSettings = array[2], - extensions = array[3], + libraryEntries = array[0], + categories = array[1], + appSettings = array[2], + extensionRepoSettings = array[3], + sourceSettings = array[4], + extensions = array[5], ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeCategoriesRestorer.kt similarity index 52% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeCategoriesRestorer.kt index 6cd92f245e..0b7a54f2e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeCategoriesRestorer.kt @@ -2,22 +2,18 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers import eu.kanade.tachiyomi.data.backup.models.BackupCategory import tachiyomi.data.handlers.anime.AnimeDatabaseHandler -import tachiyomi.data.handlers.manga.MangaDatabaseHandler import tachiyomi.domain.category.anime.interactor.GetAnimeCategories -import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.library.service.LibraryPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class CategoriesRestorer( +class AnimeCategoriesRestorer( private val animeHandler: AnimeDatabaseHandler = Injekt.get(), - private val mangaHandler: MangaDatabaseHandler = Injekt.get(), private val getAnimeCategories: GetAnimeCategories = Injekt.get(), - private val getMangaCategories: GetMangaCategories = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), ) { - suspend fun restoreAnimeCategories(backupCategories: List) { + suspend operator fun invoke(backupCategories: List) { if (backupCategories.isNotEmpty()) { val dbCategories = getAnimeCategories.await() val dbCategoriesByName = dbCategories.associateBy { it.name } @@ -44,32 +40,4 @@ class CategoriesRestorer( ) } } - - suspend fun restoreMangaCategories(backupCategories: List) { - if (backupCategories.isNotEmpty()) { - val dbCategories = getMangaCategories.await() - val dbCategoriesByName = dbCategories.associateBy { it.name } - var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0 - - val categories = backupCategories - .sortedBy { it.order } - .distinctBy { it.name } - .map { - val dbCategory = dbCategoriesByName[it.name] - if (dbCategory != null) return@map dbCategory - val order = nextOrder++ - mangaHandler.awaitOneExecutable { - categoriesQueries.insert(it.name, order, it.flags) - categoriesQueries.selectLastInsertedRowId() - } - .let { id -> it.toCategory(id).copy(order = order) } - } - - libraryPreferences.categorizedDisplaySettings().set( - (dbCategories + categories) - .distinctBy { it.flags } - .size > 1, - ) - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeExtensionRepoRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeExtensionRepoRestorer.kt new file mode 100644 index 0000000000..e53bcc78ab --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeExtensionRepoRestorer.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import mihon.domain.extensionrepo.anime.interactor.GetAnimeExtensionRepo +import tachiyomi.data.handlers.anime.AnimeDatabaseHandler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class AnimeExtensionRepoRestorer( + private val animeHandler: AnimeDatabaseHandler = Injekt.get(), + private val getExtensionRepos: GetAnimeExtensionRepo = 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 { + animeHandler.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/AnimeRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeRestorer.kt index 7702ea2dbd..467855a6e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/AnimeRestorer.kt @@ -54,7 +54,7 @@ class AnimeRestorer( ) } - suspend fun restoreAnime( + suspend fun restore( backupAnime: BackupAnime, backupCategories: List, ) { @@ -72,7 +72,7 @@ class AnimeRestorer( episodes = backupAnime.episodes, categories = backupAnime.categories, backupCategories = backupCategories, - history = backupAnime.history + backupAnime.brokenHistory.map { it.toBackupHistory() }, + history = backupAnime.history, tracks = backupAnime.tracking, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaCategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaCategoriesRestorer.kt new file mode 100644 index 0000000000..2ffdcd4690 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaCategoriesRestorer.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import tachiyomi.data.handlers.manga.MangaDatabaseHandler +import tachiyomi.domain.category.manga.interactor.GetMangaCategories +import tachiyomi.domain.library.service.LibraryPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaCategoriesRestorer( + private val mangaHandler: MangaDatabaseHandler = Injekt.get(), + private val getMangaCategories: GetMangaCategories = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), +) { + + suspend operator fun invoke(backupCategories: List) { + if (backupCategories.isNotEmpty()) { + val dbCategories = getMangaCategories.await() + val dbCategoriesByName = dbCategories.associateBy { it.name } + var nextOrder = dbCategories.maxOfOrNull { it.order }?.plus(1) ?: 0 + + val categories = backupCategories + .sortedBy { it.order } + .map { + val dbCategory = dbCategoriesByName[it.name] + if (dbCategory != null) return@map dbCategory + val order = nextOrder++ + mangaHandler.awaitOneExecutable { + categoriesQueries.insert(it.name, order, it.flags) + categoriesQueries.selectLastInsertedRowId() + } + .let { id -> it.toCategory(id).copy(order = order) } + } + + libraryPreferences.categorizedDisplaySettings().set( + (dbCategories + categories) + .distinctBy { it.flags } + .size > 1, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaExtensionRepoRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaExtensionRepoRestorer.kt new file mode 100644 index 0000000000..dfe5caaec7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaExtensionRepoRestorer.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import mihon.domain.extensionrepo.manga.interactor.GetMangaExtensionRepo +import tachiyomi.data.handlers.manga.MangaDatabaseHandler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaExtensionRepoRestorer( + private val mangaHandler: MangaDatabaseHandler = Injekt.get(), + private val getExtensionRepos: GetMangaExtensionRepo = 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 { + mangaHandler.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/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index fd012561a1..33ae6ba737 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -54,7 +54,7 @@ class MangaRestorer( ) } - suspend fun restoreManga( + suspend fun restore( backupManga: BackupManga, backupCategories: List, ) { @@ -72,7 +72,7 @@ class MangaRestorer( chapters = backupManga.chapters, categories = backupManga.categories, backupCategories = backupCategories, - history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, + history = backupManga.history, tracks = backupManga.tracking, excludedScanlators = backupManga.excludedScanlators, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt index 694a0cce37..79ba85fadd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt @@ -23,7 +23,7 @@ class PreferenceRestorer( private val preferenceStore: PreferenceStore = Injekt.get(), ) { - fun restoreAppPreferences(preferences: List) { + fun restoreApp(preferences: List) { restorePreferences(preferences, preferenceStore) AnimeLibraryUpdateJob.setupTask(context) @@ -31,7 +31,7 @@ class PreferenceRestorer( BackupCreateJob.setupTask(context) } - fun restoreSourcePreferences(preferences: List) { + fun restoreSource(preferences: List) { preferences.forEach { val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) restorePreferences(it.prefs, sourcePrefs) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/AnimeCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/AnimeCoverFetcher.kt index 12c6b1e6d4..a8d7deb3e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/AnimeCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/AnimeCoverFetcher.kt @@ -45,7 +45,6 @@ import java.io.IOException * Available request parameter: * - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true */ -@Suppress("LongParameterList") class AnimeCoverFetcher( private val url: String?, private val isLibraryAnime: Boolean, @@ -291,7 +290,9 @@ class AnimeCoverFetcher( } private enum class Type { - File, URL, URI + File, + URL, + URI, } class AnimeFactory( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index e959c3e6c1..e4dbeaacb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -45,7 +45,6 @@ import java.io.IOException * Available request parameter: * - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true */ -@Suppress("LongParameterList") class MangaCoverFetcher( private val url: String?, private val isLibraryManga: Boolean, @@ -291,7 +290,9 @@ class MangaCoverFetcher( } private enum class Type { - File, URL, URI + File, + URL, + URI, } class MangaFactory( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCModels.kt index 0cc750fdde..eb310ae355 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCModels.kt @@ -7,29 +7,25 @@ package eu.kanade.tachiyomi.data.connections.discord import androidx.annotation.StringRes -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement // Constant for logging tag -@Suppress("TopLevelPropertyNaming") const val RICH_PRESENCE_TAG = "discord_rpc" // Constant for application id -@Suppress("TopLevelPropertyNaming") private const val RICH_PRESENCE_APPLICATION_ID = "1173423931865170070" // Constant for buttons list -@Suppress("TopLevelPropertyNaming") private val RICH_PRESENCE_BUTTONS = listOf("Discord Server") // Constant for metadata list -@Suppress("TopLevelPropertyNaming") private val RICH_PRESENCE_METADATA = Activity.Metadata( listOf( - "https://discord.gg/vN8nbPHzeC" + "https://discord.gg/vN8nbPHzeC", ), ) @@ -183,29 +179,29 @@ enum class DiscordScreen( @StringRes val details: Int, val imageUrl: String, ) { - APP(R.string.app_name, R.string.browsing, AnimetailImage), - LIBRARY(R.string.label_library, R.string.browsing, LibraryImageUrl), - UPDATES(R.string.label_recent_updates, R.string.scrolling, UpdatesImageUrl), - HISTORY(R.string.label_recent_manga, R.string.scrolling, HistoryImageUrl), - BROWSE(R.string.label_sources, R.string.browsing, BrowseImageUrl), - MORE(R.string.label_settings, R.string.messing, MoreImageUrl), - WEBVIEW(R.string.action_web_view, R.string.browsing, WebviewImageUrl), - VIDEO(R.string.video, R.string.watching, VideoImageUrl), - MANGA(R.string.manga, R.string.reading, MangaImageUrl), + APP(R.string.app_name, R.string.browsing, ANIMETAIL_IMAGE), + LIBRARY(R.string.label_library, R.string.browsing, LIBRARY_IMAGE_URL), + UPDATES(R.string.label_recent_updates, R.string.scrolling, UPDATES_IMAGE_URL), + HISTORY(R.string.label_recent_manga, R.string.scrolling, HISTORY_IMAGE_URL), + BROWSE(R.string.label_sources, R.string.browsing, BROWSE_IMAGE_URL), + MORE(R.string.label_settings, R.string.messing, MORE_IMAGE_URL), + WEBVIEW(R.string.action_web_view, R.string.browsing, WEBVIEW_IMAGE_URL), + VIDEO(R.string.video, R.string.watching, VIDEO_IMAGE_URL), + MANGA(R.string.manga, R.string.reading, MANGA_IMAGE_URL), } // Constants for standard Rich Presence image urls // change the image Urls used here to match animetail brown/ green theme, Luft -private const val AnimetailImageUrl = "emojis/1286834441981005824.webp?quality=lossless" -private const val AnimetailPreviewImageUrl = "emojis/1286834519533420544.webp?quality=lossless" -private val AnimetailImage = if (BuildConfig.PREVIEW == true) AnimetailPreviewImageUrl else AnimetailImageUrl -private const val LibraryImageUrl = "emojis/1235353629867638924.webp?quality=lossless" -private const val UpdatesImageUrl = "emojis/1235354596570955917.webp?quality=lossless" -private const val HistoryImageUrl = "emojis/1235354299089817671.webp?quality=lossless" -private const val BrowseImageUrl = "emojis/1235354864419344455.webp?quality=lossless" -private const val MoreImageUrl = "emojis/1235355169752088706.webp?quality=lossless" -private const val WebviewImageUrl = "emojis/1235355362169851996.webp?quality=lossless" -private const val VideoImageUrl = "emojis/1235355607201218660.webp?quality=lossless" -private const val MangaImageUrl = "emojis/1235355804274659390.webp?quality=lossless" +private const val ANIMETAIL_IMAGE_URL = "emojis/1286834441981005824.webp?quality=lossless" +private const val ANIMETAIL_PREVIEW_IMAGE_URL = "emojis/1286834519533420544.webp?quality=lossless" +private val ANIMETAIL_IMAGE = if (BuildConfig.PREVIEW == true) ANIMETAIL_PREVIEW_IMAGE_URL else ANIMETAIL_IMAGE_URL +private const val LIBRARY_IMAGE_URL = "emojis/1235353629867638924.webp?quality=lossless" +private const val UPDATES_IMAGE_URL = "emojis/1235354596570955917.webp?quality=lossless" +private const val HISTORY_IMAGE_URL = "emojis/1235354299089817671.webp?quality=lossless" +private const val BROWSE_IMAGE_URL = "emojis/1235354864419344455.webp?quality=lossless" +private const val MORE_IMAGE_URL = "emojis/1235355169752088706.webp?quality=lossless" +private const val WEBVIEW_IMAGE_URL = "emojis/1235355362169851996.webp?quality=lossless" +private const val VIDEO_IMAGE_URL = "emojis/1235355607201218660.webp?quality=lossless" +private const val MANGA_IMAGE_URL = "emojis/1235355804274659390.webp?quality=lossless" // <-- AM (DISCORD) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCService.kt index 029d5a8add..b6383b054a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordRPCService.kt @@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.ui.player.viewer.PipState import eu.kanade.tachiyomi.util.system.notificationBuilder import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.serialization.json.Json import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.category.anime.interactor.GetAnimeCategories @@ -30,7 +31,6 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import kotlin.math.ceil import kotlin.math.floor -import kotlinx.serialization.json.Json class DiscordRPCService : Service() { @@ -237,11 +237,17 @@ class DiscordRPCService : Service() { allowStructuredMapKeys = true ignoreUnknownKeys = true } - val rpcExternalAsset = RPCExternalAsset(applicationId = RICH_PRESENCE_APPLICATION_ID, token = connectionsPreferences.connectionsToken(connectionsManager.discord).get(), client = client, json = json) + val rpcExternalAsset = + RPCExternalAsset( + applicationId = RICH_PRESENCE_APPLICATION_ID, + token = connectionsPreferences.connectionsToken(connectionsManager.discord).get(), + client = client, + json = json, + ) val discordUri = if (!discordIncognito) { try { - rpcExternalAsset.getDiscordUri(playerData.thumbnailUrl) + rpcExternalAsset.getDiscordUri(playerData.thumbnailUrl) } catch (e: Throwable) { null } @@ -264,7 +270,6 @@ class DiscordRPCService : Service() { } } - @Suppress("SwallowedException", "TooGenericExceptionCaught", "CyclomaticComplexMethod") internal suspend fun setReaderActivity( context: Context, @@ -303,8 +308,14 @@ class DiscordRPCService : Service() { val connectionsManager: ConnectionsManager by injectLazy() val networkService: NetworkHelper by injectLazy() val client = networkService.client - val json = Json { ignoreUnknownKeys = true } // Configura el JSON parser si es necesario - val rpcExternalAsset = RPCExternalAsset(applicationId = RICH_PRESENCE_APPLICATION_ID , token = connectionsPreferences.connectionsToken(connectionsManager.discord).get(), client = client, json = json) + val json = Json { ignoreUnknownKeys = true } // Configura el JSON parser si es necesario + val rpcExternalAsset = + RPCExternalAsset( + applicationId = RICH_PRESENCE_APPLICATION_ID, + token = connectionsPreferences.connectionsToken(connectionsManager.discord).get(), + client = client, + json = json, + ) val discordUri = if (!discordIncognito) { try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordWebsocket.kt b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordWebsocket.kt index bfa18d1fc0..461a79b095 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordWebsocket.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/DiscordWebsocket.kt @@ -132,7 +132,9 @@ open class DiscordWebSocketImpl( heartbeatInterval = map.d.jsonObject["heartbeat_interval"]!!.jsonPrimitive.long sendHeartBeat(true) } - OpCode.DISPATCH.value -> if (map.t == "READY") { connected = true } + OpCode.DISPATCH.value -> if (map.t == "READY") { + connected = true + } OpCode.HEARTBEAT.value -> { if (scope.isActive) scope.cancel() webSocket.send("{\"op\":1, \"d\":$seq}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RpcExternalAsset.kt b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RPCExternalAsset.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RpcExternalAsset.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RPCExternalAsset.kt index 2007544545..f031c1c5d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RpcExternalAsset.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/connections/discord/RPCExternalAsset.kt @@ -19,14 +19,14 @@ class RPCExternalAsset( applicationId: String, private val token: String, private val client: OkHttpClient, - private val json: Json + private val json: Json, ) { @Serializable data class ExternalAsset( val url: String? = null, @SerialName("external_asset_path") - val externalAssetPath: String? = null + val externalAssetPath: String? = null, ) private val api = "https://discord.com/api/v9/applications/$applicationId/external-assets" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrack.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrack.kt index 540d575863..a4ab9c65e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrack.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrack.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.anime import java.io.Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrackImpl.kt index ada673593e..04bd065fa9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/AnimeTrackImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.anime class AnimeTrackImpl : AnimeTrack { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/Episode.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/Episode.kt index e88bfb6878..7866b5659a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/Episode.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/Episode.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.anime import eu.kanade.tachiyomi.animesource.model.SEpisode diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/EpisodeImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/EpisodeImpl.kt index 218d9bab75..885fffe542 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/EpisodeImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/anime/EpisodeImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.anime class EpisodeImpl : Episode { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/Chapter.kt index 8c8c8ea20d..194b3967ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/Chapter.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.manga import eu.kanade.tachiyomi.source.model.SChapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/ChapterImpl.kt index 16e069664a..eb683cbcd5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/ChapterImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.manga class ChapterImpl : Chapter { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrack.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrack.kt index ec7b799c6e..3e1d51e2d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrack.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrack.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.manga import java.io.Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrackImpl.kt index 48e0fa7df6..e9688afc53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrackImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/manga/MangaTrackImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.database.models.manga class MangaTrackImpl : MangaTrack { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt index a22286a9cb..c933c1fdea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt @@ -53,7 +53,6 @@ import tachiyomi.domain.storage.service.StorageManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds @@ -98,13 +97,13 @@ class AnimeDownloadCache( private val diskCacheFile: File get() = File(context.cacheDir, "dl_index_cache_v3") - private val rootDownloadsDirLock = Mutex() + private val rootDownloadsDirMutex = Mutex() private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) init { // Attempt to read cache file scope.launch { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { try { if (diskCacheFile.exists()) { val diskCache = diskCacheFile.inputStream().use { @@ -225,7 +224,7 @@ class AnimeDownloadCache( * @param anime the anime of the episode. */ suspend fun addEpisode(episodeDirName: String, animeUniFile: UniFile, anime: Anime) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { // Retrieve the cached source directory or cache a new one var sourceDir = rootDownloadsDir.sourceDirs[anime.source] if (sourceDir == null) { @@ -257,7 +256,7 @@ class AnimeDownloadCache( * @param anime the anime of the episode. */ suspend fun removeEpisode(episode: Episode, anime: Anime) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return provider.getValidEpisodeDirNames(episode.name, episode.scanlator).forEach { @@ -277,7 +276,7 @@ class AnimeDownloadCache( * @param anime the anime of the episode. */ suspend fun removeEpisodes(episodes: List, anime: Anime) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return episodes.forEach { episode -> @@ -298,7 +297,7 @@ class AnimeDownloadCache( * @param anime the anime to remove. */ suspend fun removeAnime(anime: Anime) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return val animeDirName = provider.getAnimeDirName(anime.title) if (sourceDir.animeDirs.containsKey(animeDirName)) { @@ -310,7 +309,7 @@ class AnimeDownloadCache( } suspend fun removeSource(source: AnimeSource) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { rootDownloadsDir.sourceDirs -= source.id } @@ -351,10 +350,10 @@ class AnimeDownloadCache( provider.getSourceDirName(it).lowercase() to it.id } - rootDownloadsDirLock.withLock { - rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) + rootDownloadsDirMutex.withLock { + val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory()) - val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() + updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } .mapNotNull { dir -> val sourceId = sourceMap[dir.name!!.lowercase()] @@ -362,40 +361,36 @@ class AnimeDownloadCache( } .toMap() - rootDownloadsDir.sourceDirs = sourceDirs - - sourceDirs.values - .map { sourceDir -> - async { - val animeDirs = sourceDir.dir?.listFiles().orEmpty() - .filter { it.isDirectory && !it.name.isNullOrBlank() } - .associate { it.name!! to AnimeDirectory(it) } - - sourceDir.animeDirs = ConcurrentHashMap(animeDirs) - - sourceDir.animeDirs.values.forEach { animeDir -> - val episodeDirs = animeDir.dir?.listFiles().orEmpty() - .mapNotNull { - when { - // Ignore incomplete downloads - it.name?.endsWith(AnimeDownloader.TMP_DIR_SUFFIX) == true -> null - // Folder of videos - it.isDirectory -> it.name - // MP4 files - it.isFile && it.extension == "mp4" -> it.nameWithoutExtension - // MKV files - it.isFile && it.extension == "mkv" -> it.nameWithoutExtension - // Anything else is irrelevant - else -> null - } + updatedRootDir.sourceDirs.values.map { sourceDir -> + async { + sourceDir.animeDirs = sourceDir.dir?.listFiles().orEmpty() + .filter { it.isDirectory && !it.name.isNullOrBlank() } + .associate { it.name!! to AnimeDirectory(it) } + sourceDir.animeDirs.values.forEach { animeDir -> + val episodeDirs = animeDir.dir?.listFiles().orEmpty() + .mapNotNull { + when { + // Ignore incomplete downloads + it.name?.endsWith(AnimeDownloader.TMP_DIR_SUFFIX) == true -> null + // Folder of videos + it.isDirectory -> it.name + // MP4 files + it.isFile && it.extension == "mp4" -> it.nameWithoutExtension + // MKV files + it.isFile && it.extension == "mkv" -> it.nameWithoutExtension + // Anything else is irrelevant + else -> null } - .toMutableSet() + } + .toMutableSet() - animeDir.episodeDirs = episodeDirs - } + animeDir.episodeDirs = episodeDirs } } + } .awaitAll() + + rootDownloadsDir = updatedRootDir } _isInitializing.emit(false) @@ -426,7 +421,6 @@ class AnimeDownloadCache( private var updateDiskCacheJob: Job? = null - @Suppress("MagicNumber") private fun updateDiskCache() { updateDiskCacheJob?.cancel() updateDiskCacheJob = scope.launchIO { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadNotifier.kt index aa8c36834d..9a55996a7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadNotifier.kt @@ -85,6 +85,11 @@ internal class AnimeDownloadNotifier(private val context: Context) { context.stringResource(MR.strings.action_pause), NotificationReceiver.pauseAnimeDownloadsPendingBroadcast(context), ) + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_anime), + NotificationReceiver.openAnimeEntryPendingActivity(context, download.anime.id), + ) } val downloadingProgressText = if (download.progress == 0) { @@ -164,8 +169,10 @@ internal class AnimeDownloadNotifier(private val context: Context) { * Called when the downloader receives a warning. * * @param reason the text to show. + * @param timeout duration after which to automatically dismiss the notification. + * @param animeId the id of the entry being warned about */ - fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) { + fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null, animeId: Long? = null) { with(errorNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title)) setStyle(NotificationCompat.BigTextStyle().bigText(reason)) @@ -173,6 +180,13 @@ internal class AnimeDownloadNotifier(private val context: Context) { setAutoCancel(true) clearActions() setContentIntent(NotificationHandler.openAnimeDownloadManagerPendingActivity(context)) + if (animeId != null) { + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_anime), + NotificationReceiver.openAnimeEntryPendingActivity(context, animeId), + ) + } setProgress(0, 0, false) timeout?.let { setTimeoutAfter(it) } contentIntent?.let { setContentIntent(it) } @@ -190,8 +204,9 @@ internal class AnimeDownloadNotifier(private val context: Context) { * * @param error string containing error information. * @param episode string containing episode title. + * @param animeId the id of the entry that the error occurred on */ - fun onError(error: String? = null, episode: String? = null, animeTitle: String? = null) { + fun onError(error: String? = null, episode: String? = null, animeTitle: String? = null, animeId: Long? = null) { // Create notification with(errorNotificationBuilder) { setContentTitle( @@ -203,6 +218,13 @@ internal class AnimeDownloadNotifier(private val context: Context) { setSmallIcon(R.drawable.ic_warning_white_24dp) clearActions() setContentIntent(NotificationHandler.openAnimeDownloadManagerPendingActivity(context)) + if (animeId != null) { + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_anime), + NotificationReceiver.openAnimeEntryPendingActivity(context, animeId), + ) + } setProgress(0, 0, false) show(Notifications.ID_DOWNLOAD_EPISODE_ERROR) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadProvider.kt index 308fd3fd74..478964e9b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadProvider.kt @@ -13,11 +13,10 @@ import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem - +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get /** * This class is used to provide the directories where the downloads should be saved. @@ -211,7 +210,7 @@ class AnimeDownloadProvider( if (animeSource == null) return null return if (animeSource.isLocal()) { val (animeDirName, episodeDirName) = episodeUrl?.split('/', limit = 2) ?: return null - localFileSystem.getBaseDirectory()?.findFile(animeDirName )?.findFile(episodeDirName)?.size() + localFileSystem.getBaseDirectory()?.findFile(animeDirName)?.findFile(episodeDirName)?.size() } else { findEpisodeDir(episodeName, episodeScanlator, animeTitle, animeSource)?.size() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt index 2cc94319d5..b6759b16da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt @@ -196,7 +196,7 @@ class AnimeDownloader( fun clearQueue() { cancelDownloaderJob() - _clearQueue() + internalClearQueue() notifier.dismissProgress() } @@ -382,7 +382,7 @@ class AnimeDownloader( ensureSuccessfulAnimeDownload(download, animeDir, tmpDir, episodeDirname) } catch (e: Exception) { download.status = AnimeDownload.State.ERROR - notifier.onError(e.message, download.episode.name, download.anime.title) + notifier.onError(e.message, download.episode.name, download.anime.title, download.anime.id) } finally { notifier.dismissProgress() } @@ -497,7 +497,12 @@ class AnimeDownloader( } } } catch (e: Exception) { - notifier.onError(e.message + ", retrying..", download.episode.name, download.anime.title) + notifier.onError( + e.message + ", retrying..", + download.episode.name, + download.anime.title, + download.anime.id, + ) delay(2 * 1000L) null } @@ -517,7 +522,7 @@ class AnimeDownloader( httpDownload(download, tmpDir, filename, 1, true) } } catch (e: Exception) { - notifier.onError(e.message, download.episode.name, download.anime.title) + notifier.onError(e.message, download.episode.name, download.anime.title, download.anime.id) throw e } } else { @@ -990,7 +995,7 @@ class AnimeDownloader( while (i < sortedParts.size - 1) { val part = sortedParts[i] result.add(part) - if (part.completed && !sortedParts[i+1].completed) { + if (part.completed && !sortedParts[i + 1].completed) { part.completed = false // not completed anymore part.range = sortedParts[i].range.copy(second = sortedParts[i + 1].range.second) // extends range part.request = sortedParts[i + 1].request // Assumes that not completed parts have at least a Request @@ -1360,7 +1365,7 @@ class AnimeDownloader( removeFromQueueIf { it.anime.id == anime.id } } - private fun _clearQueue() { + private fun internalClearQueue() { _queueState.update { it.forEach { download -> if (download.status == AnimeDownload.State.DOWNLOADING || @@ -1386,7 +1391,7 @@ class AnimeDownloader( val wasRunning = isRunning pause() - _clearQueue() + internalClearQueue() addAllToQueue(downloads) if (wasRunning) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/model/AnimeDownload.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/model/AnimeDownload.kt index 5180a9c260..e2b7363211 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/model/AnimeDownload.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/model/AnimeDownload.kt @@ -33,14 +33,14 @@ data class AnimeDownload( } @Transient - private val _progressFlow = MutableStateFlow(0) + private val progressStateFlow = MutableStateFlow(0) @Transient - val progressFlow = _progressFlow.asStateFlow() + val progressFlow = progressStateFlow.asStateFlow() var progress: Int - get() = _progressFlow.value + get() = progressStateFlow.value set(value) { - _progressFlow.value = value + progressStateFlow.value = value } @Transient @@ -48,20 +48,20 @@ data class AnimeDownload( var totalContentLength: Long = 0L @Transient - private val _bytesDownloadedFlow = MutableStateFlow(0L) + private val bytesDownloadedFlow = MutableStateFlow(0L) var bytesDownloaded: Long - get() = _bytesDownloadedFlow.value + get() = bytesDownloadedFlow.value set(value) { - _bytesDownloadedFlow.value += value + bytesDownloadedFlow.value += value } /** * resets the internal progress state of download */ fun resetProgress() { - _bytesDownloadedFlow.value = 0L - _progressFlow.value = 0 + bytesDownloadedFlow.value = 0L + progressStateFlow.value = 0 } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt index f477810cb6..0d14eb8192 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt @@ -53,7 +53,6 @@ import tachiyomi.domain.storage.service.StorageManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds @@ -98,13 +97,13 @@ class MangaDownloadCache( private val diskCacheFile: File get() = File(context.cacheDir, "dl_index_cache_v3") - private val rootDownloadsDirLock = Mutex() + private val rootDownloadsDirMutex = Mutex() private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) init { // Attempt to read cache file scope.launch { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { try { if (diskCacheFile.exists()) { val diskCache = diskCacheFile.inputStream().use { @@ -114,7 +113,7 @@ class MangaDownloadCache( lastRenew = System.currentTimeMillis() } } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" } + logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" } diskCacheFile.delete() } } @@ -229,7 +228,7 @@ class MangaDownloadCache( * @param manga the manga of the chapter. */ suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { // Retrieve the cached source directory or cache a new one var sourceDir = rootDownloadsDir.sourceDirs[manga.source] if (sourceDir == null) { @@ -261,7 +260,7 @@ class MangaDownloadCache( * @param manga the manga of the chapter. */ suspend fun removeChapter(chapter: Chapter, manga: Manga) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach { @@ -281,7 +280,7 @@ class MangaDownloadCache( * @param manga the manga of the chapter. */ suspend fun removeChapters(chapters: List, manga: Manga) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return chapters.forEach { chapter -> @@ -302,7 +301,7 @@ class MangaDownloadCache( * @param manga the manga to remove. */ suspend fun removeManga(manga: Manga) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val mangaDirName = provider.getMangaDirName(manga.title) if (sourceDir.mangaDirs.containsKey(mangaDirName)) { @@ -314,7 +313,7 @@ class MangaDownloadCache( } suspend fun removeSource(source: MangaSource) { - rootDownloadsDirLock.withLock { + rootDownloadsDirMutex.withLock { rootDownloadsDir.sourceDirs -= source.id } @@ -353,10 +352,10 @@ class MangaDownloadCache( val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } - rootDownloadsDirLock.withLock { - rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) + rootDownloadsDirMutex.withLock { + val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory()) - val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() + updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } .mapNotNull { dir -> val sourceId = sourceMap[dir.name!!.lowercase()] @@ -364,38 +363,34 @@ class MangaDownloadCache( } .toMap() - rootDownloadsDir.sourceDirs = sourceDirs - - sourceDirs.values - .map { sourceDir -> - async { - val mangaDirs = sourceDir.dir?.listFiles().orEmpty() - .filter { it.isDirectory && !it.name.isNullOrBlank() } - .associate { it.name!! to MangaDirectory(it) } - - sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs) - - sourceDir.mangaDirs.values.forEach { mangaDir -> - val chapterDirs = mangaDir.dir?.listFiles().orEmpty() - .mapNotNull { - when { - // Ignore incomplete downloads - it.name?.endsWith(MangaDownloader.TMP_DIR_SUFFIX) == true -> null - // Folder of images - it.isDirectory -> it.name - // CBZ files - it.isFile && it.extension == "cbz" -> it.nameWithoutExtension - // Anything else is irrelevant - else -> null - } + updatedRootDir.sourceDirs.values.map { sourceDir -> + async { + sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() + .filter { it.isDirectory && !it.name.isNullOrBlank() } + .associate { it.name!! to MangaDirectory(it) } + sourceDir.mangaDirs.values.forEach { mangaDir -> + val chapterDirs = mangaDir.dir?.listFiles().orEmpty() + .mapNotNull { + when { + // Ignore incomplete downloads + it.name?.endsWith(MangaDownloader.TMP_DIR_SUFFIX) == true -> null + // Folder of images + it.isDirectory -> it.name + // CBZ files + it.isFile && it.extension == "cbz" -> it.nameWithoutExtension + // Anything else is irrelevant + else -> null } - .toMutableSet() + } + .toMutableSet() - mangaDir.chapterDirs = chapterDirs - } + mangaDir.chapterDirs = chapterDirs } } + } .awaitAll() + + rootDownloadsDir = updatedRootDir } _isInitializing.emit(false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadNotifier.kt index 8810b986c9..1a59577773 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadNotifier.kt @@ -83,6 +83,11 @@ internal class MangaDownloadNotifier(private val context: Context) { context.stringResource(MR.strings.action_pause), NotificationReceiver.pauseDownloadsPendingBroadcast(context), ) + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_manga), + NotificationReceiver.openMangaEntryPendingActivity(context, download.manga.id), + ) } val downloadingProgressText = context.stringResource( @@ -160,9 +165,10 @@ internal class MangaDownloadNotifier(private val context: Context) { * * @param reason the text to show. * @param timeout duration after which to automatically dismiss the notification. + * @param mangaId the id of the entry being warned about * Only works on Android 8+. */ - fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) { + fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null, mangaId: Long? = null) { with(errorNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title)) setStyle(NotificationCompat.BigTextStyle().bigText(reason)) @@ -170,6 +176,13 @@ internal class MangaDownloadNotifier(private val context: Context) { setAutoCancel(true) clearActions() setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) + if (mangaId != null) { + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_manga), + NotificationReceiver.openMangaEntryPendingActivity(context, mangaId), + ) + } setProgress(0, 0, false) timeout?.let { setTimeoutAfter(it) } contentIntent?.let { setContentIntent(it) } @@ -187,8 +200,9 @@ internal class MangaDownloadNotifier(private val context: Context) { * * @param error string containing error information. * @param chapter string containing chapter title. + * @param mangaId the id of the entry that the error occurred on */ - fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) { + fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null, mangaId: Long? = null) { // Create notification with(errorNotificationBuilder) { setContentTitle( @@ -200,6 +214,13 @@ internal class MangaDownloadNotifier(private val context: Context) { setSmallIcon(R.drawable.ic_warning_white_24dp) clearActions() setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) + if (mangaId != null) { + addAction( + R.drawable.ic_book_24dp, + context.stringResource(MR.strings.action_show_manga), + NotificationReceiver.openMangaEntryPendingActivity(context, mangaId), + ) + } setProgress(0, 0, false) show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt index 353e9ef9b5..1d28e57e85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt @@ -41,7 +41,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority -import mihon.core.common.archive.ZipWriter +import mihon.core.archive.ZipWriter import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import okio.Throttler @@ -197,7 +197,7 @@ class MangaDownloader( fun clearQueue() { cancelDownloaderJob() - _clearQueue() + internalClearQueue() notifier.dismissProgress() } @@ -340,6 +340,7 @@ class MangaDownloader( context.stringResource(MR.strings.download_insufficient_space), download.chapter.name, download.manga.title, + download.manga.id, ) return } @@ -435,7 +436,7 @@ class MangaDownloader( // If the page list threw, it will resume here logcat(LogPriority.ERROR, error) download.status = MangaDownload.State.ERROR - notifier.onError(error.message, download.chapter.name, download.manga.title) + notifier.onError(error.message, download.chapter.name, download.manga.title, download.manga.id) } } @@ -466,9 +467,10 @@ class MangaDownloader( // Try to find the image file val imageFile = tmpDir.listFiles()?.firstOrNull { - it.name!!.startsWith("$filename.") || it.name!!.startsWith( - "${filename}__001", - ) + it.name!!.startsWith("$filename.") || + it.name!!.startsWith( + "${filename}__001", + ) } try { @@ -493,7 +495,7 @@ class MangaDownloader( // Mark this page as error and allow to download the remaining page.progress = 0 page.status = Page.State.ERROR - notifier.onError(e.message, download.chapter.name, download.manga.title) + notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id) } } @@ -669,7 +671,7 @@ class MangaDownloader( chapter, urls, categories, - source.name + source.name, ) // Remove the old file @@ -731,7 +733,7 @@ class MangaDownloader( removeFromQueueIf { it.manga.id == manga.id } } - private fun _clearQueue() { + private fun internalClearQueue() { _queueState.update { it.forEach { download -> if (download.status == MangaDownload.State.DOWNLOADING || @@ -755,7 +757,7 @@ class MangaDownloader( } pause() - _clearQueue() + internalClearQueue() addAllToQueue(downloads) if (wasRunning) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt index 263f718e50..b471f2115c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/anime/AnimeLibraryUpdateJob.kt @@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.track.TrackStatus -import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.isConnectedToWifi @@ -44,13 +43,12 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import mihon.domain.items.episode.interactor.FilterEpisodesForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat -import tachiyomi.domain.category.anime.interactor.GetAnimeCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime @@ -86,17 +84,16 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa CoroutineWorker(context, workerParams) { private val sourceManager: AnimeSourceManager = Injekt.get() - private val downloadPreferences: DownloadPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() private val downloadManager: AnimeDownloadManager = Injekt.get() private val coverCache: AnimeCoverCache = Injekt.get() private val getLibraryAnime: GetLibraryAnime = Injekt.get() private val getAnime: GetAnime = Injekt.get() private val updateAnime: UpdateAnime = Injekt.get() - private val getCategories: GetAnimeCategories = Injekt.get() private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get() private val getTracks: GetAnimeTracks = Injekt.get() private val animeFetchInterval: AnimeFetchInterval = Injekt.get() + private val filterEpisodesForDownload: FilterEpisodesForDownload = Injekt.get() private val notifier = AnimeLibraryUpdateNotifier(context) @@ -350,21 +347,21 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa anime, ) { try { - val newChapters = updateAnime(anime, fetchWindow) + val newEpisodes = updateAnime(anime, fetchWindow) .sortedByDescending { it.sourceOrder } - if (newChapters.isNotEmpty()) { - val categoryIds = getCategories.await(anime.id).map { it.id } - if (anime.shouldDownloadNewEpisodes(categoryIds, downloadPreferences)) { - downloadEpisodes(anime, newChapters) + if (newEpisodes.isNotEmpty()) { + val episodesToDownload = filterEpisodesForDownload.await(anime, newEpisodes) + + if (episodesToDownload.isNotEmpty()) { hasDownloads.set(true) } libraryPreferences.newAnimeUpdatesCount() - .getAndSet { it + newChapters.size } + .getAndSet { it + newEpisodes.size } - // Convert to the manga that contains new chapters - newUpdates.add(anime to newChapters.toTypedArray()) + // Convert to the anime that contains new episodes + newUpdates.add(anime to newEpisodes.toTypedArray()) } } catch (e: Throwable) { val errorMessage = when (e) { @@ -532,7 +529,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa val constraints = Constraints( requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED - } else { NetworkType.CONNECTED }, + } else { + NetworkType.CONNECTED + }, requiresCharging = DEVICE_CHARGING in restrictions, requiresBatteryNotLow = true, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt index 3e9b3a4ab0..585e7b0f2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/manga/MangaLibraryUpdateJob.kt @@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.data.track.TrackStatus import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.isConnectedToWifi @@ -44,13 +43,12 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import mihon.domain.items.chapter.interactor.FilterChaptersForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat -import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.interactor.GetManga import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval @@ -86,17 +84,16 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa CoroutineWorker(context, workerParams) { private val sourceManager: MangaSourceManager = Injekt.get() - private val downloadPreferences: DownloadPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get() private val downloadManager: MangaDownloadManager = Injekt.get() private val coverCache: MangaCoverCache = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get() private val getManga: GetManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get() - private val getCategories: GetMangaCategories = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val getTracks: GetMangaTracks = Injekt.get() private val mangaFetchInterval: MangaFetchInterval = Injekt.get() + private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get() private val notifier = MangaLibraryUpdateNotifier(context) @@ -353,9 +350,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa .sortedByDescending { it.sourceOrder } if (newChapters.isNotEmpty()) { - val categoryIds = getCategories.await(manga.id).map { it.id } - if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) { - downloadChapters(manga, newChapters) + val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters) + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(manga, chaptersToDownload) hasDownloads.set(true) } libraryPreferences.newMangaUpdatesCount() @@ -529,7 +526,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa val constraints = Constraints( requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED - } else { NetworkType.CONNECTED }, + } else { + NetworkType.CONNECTED + }, requiresCharging = DEVICE_CHARGING in restrictions, requiresBatteryNotLow = true, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index ea3e193737..7c99134699 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -411,6 +411,8 @@ class NotificationReceiver : BroadcastReceiver() { private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER" private const val ACTION_DOWNLOAD_EPISODE = "$ID.$NAME.ACTION_DOWNLOAD_EPISODE" + private const val ACTION_OPEN_ENTRY = "$ID.$NAME.ACTION_OPEN_ENTRY" + private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS" private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS" private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" @@ -801,6 +803,44 @@ class NotificationReceiver : BroadcastReceiver() { ) } + /** + * Returns [PendingIntent] that opens the manga info controller + * + * @param context context of application + * @param mangaId id of the entry to open + */ + internal fun openMangaEntryPendingActivity(context: Context, mangaId: Long): PendingIntent { + val newIntent = Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(Constants.MANGA_EXTRA, mangaId) + .putExtra("notificationId", mangaId.hashCode()) + return PendingIntent.getActivity( + context, + mangaId.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + /** + * Returns [PendingIntent] that opens the anime info controller + * + * @param context context of application + * @param animeId id of the entry to open + */ + internal fun openAnimeEntryPendingActivity(context: Context, animeId: Long): PendingIntent { + val newIntent = Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_ANIME) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(Constants.ANIME_EXTRA, animeId) + .putExtra("notificationId", animeId.hashCode()) + return PendingIntent.getActivity( + context, + animeId.hashCode(), + newIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + /** * Returns [PendingIntent] that starts a service which stops the library update * 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 4b79399a99..5a2c549008 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 @@ -109,13 +109,15 @@ class SyncManager( ) logcat(LogPriority.DEBUG) { "Begin create backup" } + val backupManga = backupCreator.backupMangas(databaseManga, backupOptions) + val backupAnime = backupCreator.backupAnimes(databaseAnime, backupOptions) val backup = Backup( - backupManga = backupCreator.backupMangas(databaseManga, backupOptions), + backupManga = backupManga, backupCategories = backupCreator.backupMangaCategories(backupOptions), - backupAnime = backupCreator.backupAnimes(databaseAnime, backupOptions), + backupAnime = backupAnime, backupAnimeCategories = backupCreator.backupAnimeCategories(backupOptions), - backupSources = backupCreator.backupMangaSources(databaseManga), - backupAnimeSources = backupCreator.backupAnimeSources(databaseAnime), + backupSources = backupCreator.backupMangaSources(backupManga), + backupAnimeSources = backupCreator.backupAnimeSources(backupAnime), backupPreferences = backupCreator.backupAppPreferences(backupOptions), backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions), ) @@ -214,7 +216,7 @@ class SyncManager( options = RestoreOptions( appSettings = true, sourceSettings = true, - library = true, + libraryEntries = true, // Correct parameter name ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt index 7dfbbf6925..66bf2e27f6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/MangaTracker.kt @@ -73,7 +73,8 @@ interface MangaTracker { suspend fun setRemoteLastChapterRead(track: MangaTrack, chapterNumber: Int) { if (track.last_chapter_read == 0.0 && - track.last_chapter_read < chapterNumber && track.status != getRereadingStatus() + track.last_chapter_read < chapterNumber && + track.status != getRereadingStatus() ) { track.status = getReadingStatus() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt index 9daa2344d3..ad6dbace2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt @@ -47,11 +47,11 @@ enum class TrackStatus(val int: Long, @StringRes val res: Int) { when (statusLong) { Anilist.READING -> READING Anilist.WATCHING -> WATCHING - Anilist.REPEATING_ANIME -> REWATCHING - Anilist.PLANNING -> PLAN_TO_READ - Anilist.PLANNING_ANIME -> PLAN_TO_WATCH - Anilist.REPEATING -> REPEATING - Anilist.PAUSED -> PAUSED + Anilist.REWATCHING -> REWATCHING + Anilist.PLAN_TO_READ -> PLAN_TO_READ + Anilist.PLAN_TO_WATCH -> PLAN_TO_WATCH + Anilist.REREADING -> REPEATING + Anilist.ON_HOLD -> PAUSED Anilist.COMPLETED -> COMPLETED Anilist.DROPPED -> DROPPED else -> null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index 4c39de0378..e2d792c90c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableAnimeTracker import eu.kanade.tachiyomi.data.track.DeletableMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import kotlinx.collections.immutable.ImmutableList @@ -38,12 +39,12 @@ class Anilist(id: Long) : const val READING = 1L const val WATCHING = 11L const val COMPLETED = 2L - const val PAUSED = 3L + const val ON_HOLD = 3L const val DROPPED = 4L - const val PLANNING = 5L - const val PLANNING_ANIME = 15L - const val REPEATING = 6L - const val REPEATING_ANIME = 16L + const val PLAN_TO_READ = 5L + const val PLAN_TO_WATCH = 15L + const val REREADING = 6L + const val REWATCHING = 16L const val POINT_100 = "POINT_100" const val POINT_10 = "POINT_10" @@ -77,29 +78,29 @@ class Anilist(id: Long) : override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getStatusListManga(): List { - return listOf(READING, PLANNING, COMPLETED, REPEATING, PAUSED, DROPPED) + return listOf(READING, PLAN_TO_READ, COMPLETED, REREADING, ON_HOLD, DROPPED) } override fun getStatusListAnime(): List { - return listOf(WATCHING, PLANNING_ANIME, COMPLETED, REPEATING_ANIME, PAUSED, DROPPED) + return listOf(WATCHING, PLAN_TO_WATCH, COMPLETED, REWATCHING, ON_HOLD, DROPPED) } override fun getStatusForManga(status: Long): StringResource? = when (status) { READING -> MR.strings.reading - PLANNING -> MR.strings.plan_to_read + PLAN_TO_READ -> MR.strings.plan_to_read COMPLETED -> MR.strings.completed - REPEATING -> MR.strings.repeating - PAUSED -> MR.strings.paused + REREADING -> MR.strings.repeating + ON_HOLD -> MR.strings.paused DROPPED -> MR.strings.dropped else -> null } override fun getStatusForAnime(status: Long): StringResource? = when (status) { WATCHING -> MR.strings.watching - PLANNING_ANIME -> MR.strings.plan_to_watch + PLAN_TO_WATCH -> MR.strings.plan_to_watch COMPLETED -> MR.strings.completed - REPEATING_ANIME -> MR.strings.repeating_anime - PAUSED -> MR.strings.paused + REWATCHING -> MR.strings.repeating_anime + ON_HOLD -> MR.strings.paused DROPPED -> MR.strings.dropped else -> null } @@ -108,9 +109,9 @@ class Anilist(id: Long) : override fun getWatchingStatus(): Long = WATCHING - override fun getRereadingStatus(): Long = REPEATING + override fun getRereadingStatus(): Long = REREADING - override fun getRewatchingStatus(): Long = REPEATING_ANIME + override fun getRewatchingStatus(): Long = REWATCHING override fun getCompletionStatus(): Long = COMPLETED @@ -176,7 +177,7 @@ class Anilist(id: Long) : score <= 60 -> "😐" else -> "😊" } - else -> track.toAnilistScore() + else -> track.toApiScore() } } @@ -194,7 +195,7 @@ class Anilist(id: Long) : score <= 60 -> "😐" else -> "😊" } - else -> track.toAnilistScore() + else -> track.toApiScore() } } @@ -219,7 +220,7 @@ class Anilist(id: Long) : if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { track.status = COMPLETED track.finished_reading_date = System.currentTimeMillis() - } else if (track.status != REPEATING) { + } else if (track.status != REREADING) { track.status = READING if (track.last_chapter_read == 1.0) { track.started_reading_date = System.currentTimeMillis() @@ -244,7 +245,7 @@ class Anilist(id: Long) : if (track.last_episode_seen.toLong() == track.total_episodes && track.total_episodes > 0) { track.status = COMPLETED track.finished_watching_date = System.currentTimeMillis() - } else if (track.status != REPEATING_ANIME) { + } else if (track.status != REWATCHING) { track.status = WATCHING if (track.last_episode_seen == 1.0) { track.started_watching_date = System.currentTimeMillis() @@ -281,14 +282,14 @@ class Anilist(id: Long) : track.library_id = remoteTrack.library_id if (track.status != COMPLETED) { - val isRereading = track.status == REPEATING + val isRereading = track.status == REREADING track.status = if (!isRereading && hasReadChapters) READING else track.status } update(track) } else { // Set default fields if it's not found in the list - track.status = if (hasReadChapters) READING else PLANNING + track.status = if (hasReadChapters) READING else PLAN_TO_READ track.score = 0.0 add(track) } @@ -301,14 +302,14 @@ class Anilist(id: Long) : track.library_id = remoteTrack.library_id if (track.status != COMPLETED) { - val isRereading = track.status == REPEATING_ANIME + val isRereading = track.status == REWATCHING track.status = if (!isRereading && hasReadChapters) WATCHING else track.status } update(track) } else { // Set default fields if it's not found in the list - track.status = if (hasReadChapters) WATCHING else PLANNING_ANIME + track.status = if (hasReadChapters) WATCHING else PLAN_TO_WATCH track.score = 0.0 add(track) } @@ -346,7 +347,7 @@ class Anilist(id: Long) : interceptor.setAuth(oauth) val (username, scoreType) = api.getCurrentUser() scorePreference.set(scoreType) - saveCredentials(username.toString(), oauth.access_token) + saveCredentials(username.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } @@ -358,13 +359,13 @@ class Anilist(id: Long) : interceptor.setAuth(null) } - fun saveOAuth(oAuth: OAuth?) { - trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) + fun saveOAuth(alOAuth: ALOAuth?) { + trackPreferences.trackToken(this).set(json.encodeToString(alOAuth)) } - fun loadOAuth(): OAuth? { + fun loadOAuth(): ALOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } 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 98e74d66b7..43db958d4a 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 @@ -4,6 +4,11 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddEntryResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth +import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult +import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListEntryQueryResult import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import eu.kanade.tachiyomi.network.POST @@ -15,14 +20,6 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.intOrNull -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.OkHttpClient @@ -30,7 +27,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.common.util.lang.withIOContext import uy.kohesive.injekt.injectLazy import java.time.Instant -import java.time.LocalDate import java.time.ZoneId import java.time.ZonedDateTime import kotlin.time.Duration.Companion.minutes @@ -50,10 +46,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return withIOContext { val query = """ |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { - |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { - | id - | status - |} + |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { + | id + | status + |} |} | """.trimMargin() @@ -62,21 +58,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("mangaId", track.remote_id) put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) } } with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.library_id = - it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long track } } @@ -106,13 +100,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("listId", track.library_id) put("progress", track.last_chapter_read.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) put("score", track.score.toInt()) put("startedAt", createDate(track.started_reading_date)) put("completedAt", createDate(track.finished_reading_date)) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() track } @@ -122,9 +116,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { withIOContext { val query = """ |mutation DeleteManga(${'$'}listId: Int) { - |DeleteMediaListEntry(id: ${'$'}listId) { + |DeleteMediaListEntry(id: ${'$'}listId) { |deleted - |} + |} |} | """.trimMargin() @@ -134,7 +128,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("listId", track.libraryId) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() } } @@ -143,10 +137,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return withIOContext { val query = """ |mutation AddAnime(${'$'}animeId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { - |SaveMediaListEntry (mediaId: ${'$'}animeId, progress: ${'$'}progress, status: ${'$'}status) { - | id - | status - |} + |SaveMediaListEntry (mediaId: ${'$'}animeId, progress: ${'$'}progress, status: ${'$'}status) { + | id + | status + |} |} | """.trimMargin() @@ -155,21 +149,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("animeId", track.remote_id) put("progress", track.last_episode_seen.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) } } with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.library_id = - it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long track } } @@ -199,13 +191,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { putJsonObject("variables") { put("listId", track.library_id) put("progress", track.last_episode_seen.toInt()) - put("status", track.toAnilistStatus()) + put("status", track.toApiStatus()) put("score", track.score.toInt()) put("startedAt", createDate(track.started_watching_date)) put("completedAt", createDate(track.finished_watching_date)) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() track } @@ -215,9 +207,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return withIOContext { val query = """ |mutation DeleteAnime(${'$'}listId: Int) { - |DeleteMediaListEntry(id: ${'$'}listId) { + |DeleteMediaListEntry(id: ${'$'}listId) { |deleted - |} + |} |} | """.trimMargin() @@ -227,7 +219,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { put("listId", track.libraryId) } } - authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) + authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() } } @@ -269,19 +261,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["media"]!!.jsonArray - val entries = media.map { jsonToALManga(it.jsonObject) } - entries.map { it.toTrack() } - } + .parseAs() + .data.page.media + .map { it.toALManga().toTrack() } } } } @@ -323,19 +310,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["media"]!!.jsonArray - val entries = media.map { jsonToALAnime(it.jsonObject) } - entries.map { it.toTrack() } - } + .parseAs() + .data.page.media + .map { it.toALAnime().toTrack() } } } } @@ -393,19 +375,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["mediaList"]!!.jsonArray - val entries = media.map { jsonToALUserManga(it.jsonObject) } - entries.firstOrNull()?.toTrack() - } + .parseAs() + .data.page.mediaList + .map { it.toALUserManga() } + .firstOrNull() + ?.toTrack() } } } @@ -463,19 +442,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() - .let { response -> - val data = response["data"]!!.jsonObject - val page = data["Page"]!!.jsonObject - val media = page["mediaList"]!!.jsonArray - val entries = media.map { jsonToALUserAnime(it.jsonObject) } - entries.firstOrNull()?.toTrack() - } + .parseAs() + .data.page.mediaList + .map { it.toALUserAnime() } + .firstOrNull() + ?.toTrack() } } } @@ -488,8 +464,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { return findLibAnime(track, userId) ?: throw Exception("Could not find anime") } - fun createOAuth(token: String): OAuth { - return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) + fun createOAuth(token: String): ALOAuth { + return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) } suspend fun getCurrentUser(): Pair { @@ -511,92 +487,20 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { with(json) { authClient.newCall( POST( - apiUrl, + API_URL, body = payload.toString().toRequestBody(jsonMime), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonObject - val viewer = data["Viewer"]!!.jsonObject - Pair( - viewer["id"]!!.jsonPrimitive.int, - viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content, - ) + val viewer = it.data.viewer + Pair(viewer.id, viewer.mediaListOptions.scoreFormat) } } } } - private fun jsonToALManga(struct: JsonObject): ALManga { - return ALManga( - struct["id"]!!.jsonPrimitive.long, - struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content, - struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content, - struct["description"]!!.jsonPrimitive.contentOrNull, - struct["format"]!!.jsonPrimitive.content.replace("_", "-"), - struct["status"]!!.jsonPrimitive.contentOrNull ?: "", - parseDate(struct, "startDate"), - struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0, - struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1, - ) - } - - private fun jsonToALAnime(struct: JsonObject): ALAnime { - return ALAnime( - struct["id"]!!.jsonPrimitive.long, - struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content, - struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content, - struct["description"]!!.jsonPrimitive.contentOrNull, - struct["format"]!!.jsonPrimitive.content.replace("_", "-"), - struct["status"]!!.jsonPrimitive.contentOrNull ?: "", - parseDate(struct, "startDate"), - struct["episodes"]!!.jsonPrimitive.longOrNull ?: 0, - struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1, - ) - } - - private fun jsonToALUserManga(struct: JsonObject): ALUserManga { - return ALUserManga( - struct["id"]!!.jsonPrimitive.long, - struct["status"]!!.jsonPrimitive.content, - struct["scoreRaw"]!!.jsonPrimitive.int, - struct["progress"]!!.jsonPrimitive.int, - parseDate(struct, "startedAt"), - parseDate(struct, "completedAt"), - jsonToALManga(struct["media"]!!.jsonObject), - ) - } - - private fun jsonToALUserAnime(struct: JsonObject): ALUserAnime { - return ALUserAnime( - struct["id"]!!.jsonPrimitive.long, - struct["status"]!!.jsonPrimitive.content, - struct["scoreRaw"]!!.jsonPrimitive.int, - struct["progress"]!!.jsonPrimitive.int, - parseDate(struct, "startedAt"), - parseDate(struct, "completedAt"), - jsonToALAnime(struct["media"]!!.jsonObject), - ) - } - - private fun parseDate(struct: JsonObject, dateKey: String): Long { - return try { - return LocalDate - .of( - struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, - struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int, - struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int, - ) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli() - } catch (_: Exception) { - 0L - } - } - private fun createDate(dateValue: Long): JsonObject { if (dateValue == 0L) { return buildJsonObject { @@ -615,22 +519,22 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } companion object { - private const val clientId = "18376" - private const val apiUrl = "https://graphql.anilist.co/" - private const val baseUrl = "https://anilist.co/api/v2/" - private const val baseMangaUrl = "https://anilist.co/manga/" - private const val baseAnimeUrl = "https://anilist.co/anime/" + private const val CLIENT_ID = "18376" + private const val API_URL = "https://graphql.anilist.co/" + private const val BASE_URL = "https://anilist.co/api/v2/" + private const val BASE_MANGA_URL = "https://anilist.co/manga/" + private const val BASE_ANIME_URL = "https://anilist.co/anime/" fun mangaUrl(mediaId: Long): String { - return baseMangaUrl + mediaId + return BASE_MANGA_URL + mediaId } fun animeUrl(mediaId: Long): String { - return baseAnimeUrl + mediaId + return BASE_ANIME_URL + mediaId } - fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon() - .appendQueryParameter("client_id", clientId) + fun authUrl(): Uri = "${BASE_URL}oauth/authorize".toUri().buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("response_type", "token") .build() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt index 231d6c9661..4e8417ae5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.anilist import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth +import eu.kanade.tachiyomi.data.track.anilist.dto.isExpired import okhttp3.Interceptor import okhttp3.Response import java.io.IOException @@ -13,7 +15,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute * before its original expiration date. */ - private var oauth: OAuth? = null + private var oauth: ALOAuth? = null set(value) { field = value?.copy(expires = value.expires * 1000 - 60 * 1000) } @@ -40,7 +42,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Animetail v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() @@ -51,8 +53,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int * Called when the user authenticates with Anilist for the first time. Sets the refresh token * and the oauth object. */ - fun setAuth(oauth: OAuth?) { - token = oauth?.access_token + fun setAuth(oauth: ALOAuth?) { + token = oauth?.accessToken this.oauth = oauth anilist.saveOAuth(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt deleted file mode 100644 index 80b0dcb6d2..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ /dev/null @@ -1,231 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist - -import eu.kanade.domain.track.service.TrackPreferences -import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack -import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack -import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch -import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch -import eu.kanade.tachiyomi.util.lang.htmlDecode -import kotlinx.serialization.Serializable -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.Locale -import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack -import tachiyomi.domain.track.manga.model.MangaTrack as DomainMangaTrack - -data class ALManga( - val remote_id: Long, - val title_user_pref: String, - val image_url_lge: String, - val description: String?, - val format: String, - val publishing_status: String, - val start_date_fuzzy: Long, - val total_chapters: Long, - val average_score: Int, -) { - - fun toTrack() = MangaTrackSearch.create(TrackerManager.ANILIST).apply { - remote_id = this@ALManga.remote_id - title = title_user_pref - total_chapters = this@ALManga.total_chapters - cover_url = image_url_lge - summary = description?.htmlDecode() ?: "" - score = average_score.toDouble() - tracking_url = AnilistApi.mangaUrl(remote_id) - publishing_status = this@ALManga.publishing_status - publishing_type = format - if (start_date_fuzzy != 0L) { - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(start_date_fuzzy) - } catch (e: Exception) { - "" - } - } - } -} - -data class ALAnime( - val remote_id: Long, - val title_user_pref: String, - val image_url_lge: String, - val description: String?, - val format: String, - val publishing_status: String, - val start_date_fuzzy: Long, - val total_episodes: Long, - val average_score: Int, -) { - - fun toTrack() = AnimeTrackSearch.create(TrackerManager.ANILIST).apply { - remote_id = this@ALAnime.remote_id - title = title_user_pref - total_episodes = this@ALAnime.total_episodes - cover_url = image_url_lge - summary = description?.htmlDecode() ?: "" - score = average_score.toDouble() - tracking_url = AnilistApi.animeUrl(remote_id) - publishing_status = this@ALAnime.publishing_status - publishing_type = format - if (start_date_fuzzy != 0L) { - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(start_date_fuzzy) - } catch (e: Exception) { - "" - } - } - } -} - -data class ALUserManga( - val library_id: Long, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val start_date_fuzzy: Long, - val completed_date_fuzzy: Long, - val manga: ALManga, -) { - - fun toTrack() = MangaTrack.create(TrackerManager.ANILIST).apply { - remote_id = manga.remote_id - title = manga.title_user_pref - status = toTrackStatus() - score = score_raw.toDouble() - started_reading_date = start_date_fuzzy - finished_reading_date = completed_date_fuzzy - last_chapter_read = chapters_read.toDouble() - library_id = this@ALUserManga.library_id - total_chapters = manga.total_chapters - } - - private fun toTrackStatus() = when (list_status) { - "CURRENT" -> Anilist.READING - "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.PAUSED - "DROPPED" -> Anilist.DROPPED - "PLANNING" -> Anilist.PLANNING - "REPEATING" -> Anilist.REPEATING - else -> throw NotImplementedError("Unknown status: $list_status") - } -} - -data class ALUserAnime( - val library_id: Long, - val list_status: String, - val score_raw: Int, - val episodes_seen: Int, - val start_date_fuzzy: Long, - val completed_date_fuzzy: Long, - val anime: ALAnime, -) { - - fun toTrack() = AnimeTrack.create(TrackerManager.ANILIST).apply { - remote_id = anime.remote_id - title = anime.title_user_pref - status = toTrackStatus() - score = score_raw.toDouble() - started_watching_date = start_date_fuzzy - finished_watching_date = completed_date_fuzzy - last_episode_seen = episodes_seen.toDouble() - library_id = this@ALUserAnime.library_id - total_episodes = anime.total_episodes - } - - private fun toTrackStatus() = when (list_status) { - "CURRENT" -> Anilist.WATCHING - "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.PAUSED - "DROPPED" -> Anilist.DROPPED - "PLANNING" -> Anilist.PLANNING_ANIME - "REPEATING" -> Anilist.REPEATING_ANIME - else -> throw NotImplementedError("Unknown status: $list_status") - } -} - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val expires: Long, - val expires_in: Long, -) - -fun OAuth.isExpired() = System.currentTimeMillis() > expires - -fun MangaTrack.toAnilistStatus() = when (status) { - Anilist.READING -> "CURRENT" - Anilist.COMPLETED -> "COMPLETED" - Anilist.PAUSED -> "PAUSED" - Anilist.DROPPED -> "DROPPED" - Anilist.PLANNING -> "PLANNING" - Anilist.REPEATING -> "REPEATING" - else -> throw NotImplementedError("Unknown status: $status") -} - -fun AnimeTrack.toAnilistStatus() = when (status) { - Anilist.WATCHING -> "CURRENT" - Anilist.COMPLETED -> "COMPLETED" - Anilist.PAUSED -> "PAUSED" - Anilist.DROPPED -> "DROPPED" - Anilist.PLANNING_ANIME -> "PLANNING" - Anilist.REPEATING_ANIME -> "REPEATING" - else -> throw NotImplementedError("Unknown status: $status") -} - -private val preferences: TrackPreferences by injectLazy() - -fun DomainMangaTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { - // 10 point - "POINT_10" -> (score.toInt() / 10).toString() - // 100 point - "POINT_100" -> score.toInt().toString() - // 5 stars - "POINT_5" -> when { - score == 0.0 -> "0" - score < 30 -> "1" - score < 50 -> "2" - score < 70 -> "3" - score < 90 -> "4" - else -> "5" - } - // Smiley - "POINT_3" -> when { - score == 0.0 -> "0" - score <= 35 -> ":(" - score <= 60 -> ":|" - else -> ":)" - } - // 10 point decimal - "POINT_10_DECIMAL" -> (score / 10).toString() - else -> throw NotImplementedError("Unknown score type") -} - -fun DomainAnimeTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) { - // 10 point - "POINT_10" -> (score.toInt() / 10).toString() - // 100 point - "POINT_100" -> score.toInt().toString() - // 5 stars - "POINT_5" -> when { - score == 0.0 -> "0" - score < 30 -> "1" - score < 50 -> "2" - score < 70 -> "3" - score < 90 -> "4" - else -> "5" - } - // Smiley - "POINT_3" -> when { - score == 0.0 -> "0" - score <= 35 -> ":(" - score <= 60 -> ":|" - else -> ":)" - } - // 10 point decimal - "POINT_10_DECIMAL" -> (score / 10).toString() - else -> throw NotImplementedError("Unknown score type") -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt new file mode 100644 index 0000000000..0d048ee4b5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -0,0 +1,59 @@ +package eu.kanade.tachiyomi.data.track.anilist + +import eu.kanade.domain.track.service.TrackPreferences +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import uy.kohesive.injekt.injectLazy +import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack +import tachiyomi.domain.track.manga.model.MangaTrack as DomainMangaTrack + +fun MangaTrack.toApiStatus() = when (status) { + Anilist.READING -> "CURRENT" + Anilist.COMPLETED -> "COMPLETED" + Anilist.ON_HOLD -> "PAUSED" + Anilist.DROPPED -> "DROPPED" + Anilist.PLAN_TO_READ -> "PLANNING" + Anilist.REREADING -> "REPEATING" + else -> throw NotImplementedError("Unknown status: $status") +} + +fun AnimeTrack.toApiStatus() = when (status) { + Anilist.WATCHING -> "CURRENT" + Anilist.COMPLETED -> "COMPLETED" + Anilist.ON_HOLD -> "PAUSED" + Anilist.DROPPED -> "DROPPED" + Anilist.PLAN_TO_WATCH -> "PLANNING" + Anilist.REWATCHING -> "REPEATING" + else -> throw NotImplementedError("Unknown status: $status") +} + +private val preferences: TrackPreferences by injectLazy() + +private fun Double.toApiScore(): String = when (preferences.anilistScoreType().get()) { + // 10 point + "POINT_10" -> (this.toInt() / 10).toString() + // 100 point + "POINT_100" -> this.toInt().toString() + // 5 stars + "POINT_5" -> when { + this == 0.0 -> "0" + this < 30 -> "1" + this < 50 -> "2" + this < 70 -> "3" + this < 90 -> "4" + else -> "5" + } + // Smiley + "POINT_3" -> when { + this == 0.0 -> "0" + this <= 35 -> ":(" + this <= 60 -> ":|" + else -> ":)" + } + // 10 point decimal + "POINT_10_DECIMAL" -> (this / 10).toString() + else -> throw NotImplementedError("Unknown score type") +} + +fun DomainMangaTrack.toApiScore(): String = this.score.toApiScore() +fun DomainAnimeTrack.toApiScore(): String = this.score.toApiScore() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddEntry.kt new file mode 100644 index 0000000000..8c56e446bb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddEntry.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALAddEntryResult( + val data: ALAddEntryData, +) + +@Serializable +data class ALAddEntryData( + @SerialName("SaveMediaListEntry") + val entry: ALAddEntryEntry, +) + +@Serializable +data class ALAddEntryEntry( + val id: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAnime.kt new file mode 100644 index 0000000000..83954cd39f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAnime.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.anilist.AnilistApi +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.util.lang.htmlDecode +import java.text.SimpleDateFormat +import java.util.Locale + +data class ALAnime( + val remoteId: Long, + val title: String, + val imageUrl: String, + val description: String?, + val format: String, + val publishingStatus: String, + val startDateFuzzy: Long, + val totalEpisodes: Long, + val averageScore: Int, +) { + fun toTrack() = AnimeTrackSearch.create(TrackerManager.ANILIST).apply { + remote_id = remoteId + title = this@ALAnime.title + total_episodes = totalEpisodes + cover_url = imageUrl + summary = description?.htmlDecode() ?: "" + score = averageScore.toDouble() + tracking_url = AnilistApi.animeUrl(remote_id) + publishing_status = publishingStatus + publishing_type = format + if (startDateFuzzy != 0L) { + start_date = try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(startDateFuzzy) + } catch (e: IllegalArgumentException) { + "" + } + } + } +} + +data class ALUserAnime( + val libraryId: Long, + val listStatus: String, + val scoreRaw: Int, + val episodesSeen: Int, + val startDateFuzzy: Long, + val completedDateFuzzy: Long, + val anime: ALAnime, +) { + fun toTrack() = AnimeTrack.create(TrackerManager.ANILIST).apply { + remote_id = anime.remoteId + title = anime.title + status = toTrackStatus() + score = scoreRaw.toDouble() + started_watching_date = startDateFuzzy + finished_watching_date = completedDateFuzzy + last_episode_seen = episodesSeen.toDouble() + library_id = libraryId + total_episodes = anime.totalEpisodes + } + + private fun toTrackStatus() = when (listStatus) { + "CURRENT" -> Anilist.WATCHING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.ON_HOLD + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLAN_TO_WATCH + "REPEATING" -> Anilist.REWATCHING + else -> throw NotImplementedError("Unknown status: $listStatus") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt new file mode 100644 index 0000000000..7dbd8c2965 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable +import java.time.LocalDate +import java.time.ZoneId + +@Serializable +data class ALFuzzyDate( + val year: Int?, + val month: Int?, + val day: Int?, +) { + fun toEpochMilli(): Long = try { + LocalDate.of(year!!, month!!, day!!) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + } catch (_: Exception) { + 0L + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt new file mode 100644 index 0000000000..a4f674295a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.anilist.AnilistApi +import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.util.lang.htmlDecode +import java.text.SimpleDateFormat +import java.util.Locale + +data class ALManga( + val remoteId: Long, + val title: String, + val imageUrl: String, + val description: String?, + val format: String, + val publishingStatus: String, + val startDateFuzzy: Long, + val totalChapters: Long, + val averageScore: Int, +) { + fun toTrack() = MangaTrackSearch.create(TrackerManager.ANILIST).apply { + remote_id = remoteId + title = this@ALManga.title + total_chapters = totalChapters + cover_url = imageUrl + summary = description?.htmlDecode() ?: "" + score = averageScore.toDouble() + tracking_url = AnilistApi.mangaUrl(remote_id) + publishing_status = publishingStatus + publishing_type = format + if (startDateFuzzy != 0L) { + start_date = try { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(startDateFuzzy) + } catch (e: IllegalArgumentException) { + "" + } + } + } +} + +data class ALUserManga( + val libraryId: Long, + val listStatus: String, + val scoreRaw: Int, + val chaptersRead: Int, + val startDateFuzzy: Long, + val completedDateFuzzy: Long, + val manga: ALManga, +) { + fun toTrack() = MangaTrack.create(TrackerManager.ANILIST).apply { + remote_id = manga.remoteId + title = manga.title + status = toTrackStatus() + score = scoreRaw.toDouble() + started_reading_date = startDateFuzzy + finished_reading_date = completedDateFuzzy + last_chapter_read = chaptersRead.toDouble() + library_id = libraryId + total_chapters = manga.totalChapters + } + + private fun toTrackStatus() = when (listStatus) { + "CURRENT" -> Anilist.READING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.ON_HOLD + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLAN_TO_READ + "REPEATING" -> Anilist.REREADING + else -> throw NotImplementedError("Unknown status: $listStatus") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt new file mode 100644 index 0000000000..94fbd64000 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALOAuth.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + val expires: Long, + @SerialName("expires_in") + val expiresIn: Long, +) + +fun ALOAuth.isExpired() = System.currentTimeMillis() > expires diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt new file mode 100644 index 0000000000..f13ebb4007 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALSearchResult( + val data: ALSearchPage, +) + +@Serializable +data class ALSearchPage( + @SerialName("Page") + val page: ALSearchMedia, +) + +@Serializable +data class ALSearchMedia( + val media: List, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt new file mode 100644 index 0000000000..9d5224ce6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ALSearchItem( + val id: Long, + val title: ALItemTitle, + val coverImage: ItemCover, + val description: String?, + val format: String, + val status: String?, + val startDate: ALFuzzyDate, + val chapters: Long?, + val episodes: Long?, + val averageScore: Int?, +) { + fun toALManga(): ALManga = ALManga( + remoteId = id, + title = title.userPreferred, + imageUrl = coverImage.large, + description = description, + format = format.replace("_", "-"), + publishingStatus = status ?: "", + startDateFuzzy = startDate.toEpochMilli(), + totalChapters = chapters ?: 0, + averageScore = averageScore ?: -1, + ) + + fun toALAnime(): ALAnime = ALAnime( + remoteId = id, + title = title.userPreferred, + imageUrl = coverImage.large, + description = description, + format = format.replace("_", "-"), + publishingStatus = status ?: "", + startDateFuzzy = startDate.toEpochMilli(), + totalEpisodes = episodes ?: 0, + averageScore = averageScore ?: -1, + ) +} + +@Serializable +data class ALItemTitle( + val userPreferred: String, +) + +@Serializable +data class ItemCover( + val large: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt new file mode 100644 index 0000000000..39507a0d5f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALCurrentUserResult( + val data: ALUserViewer, +) + +@Serializable +data class ALUserViewer( + @SerialName("Viewer") + val viewer: ALUserViewerData, +) + +@Serializable +data class ALUserViewerData( + val id: Int, + val mediaListOptions: ALUserListOptions, +) + +@Serializable +data class ALUserListOptions( + val scoreFormat: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt new file mode 100644 index 0000000000..00452b03af --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt @@ -0,0 +1,55 @@ +package eu.kanade.tachiyomi.data.track.anilist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ALUserListEntryQueryResult( + val data: ALUserListEntryPage, +) + +@Serializable +data class ALUserListEntryPage( + @SerialName("Page") + val page: ALUserListMediaList, +) + +@Serializable +data class ALUserListMediaList( + val mediaList: List, +) + +@Serializable +data class ALUserListItem( + val id: Long, + val status: String, + val scoreRaw: Int, + val progress: Int, + val startedAt: ALFuzzyDate, + val completedAt: ALFuzzyDate, + val media: ALSearchItem, +) { + fun toALUserManga(): ALUserManga { + return ALUserManga( + libraryId = this@ALUserListItem.id, + listStatus = status, + scoreRaw = scoreRaw, + chaptersRead = progress, + startDateFuzzy = startedAt.toEpochMilli(), + completedDateFuzzy = completedAt.toEpochMilli(), + manga = media.toALManga(), + ) + } + + fun toALUserAnime(): ALUserAnime { + return ALUserAnime( + libraryId = this@ALUserListItem.id, + listStatus = status, + scoreRaw = scoreRaw, + episodesSeen = progress, + startDateFuzzy = startedAt.toEpochMilli(), + completedDateFuzzy = completedAt.toEpochMilli(), + anime = media.toALAnime(), + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index 7852b27768..9b0299c19f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.AnimeTracker import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.MangaTracker +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import kotlinx.collections.immutable.ImmutableList @@ -135,8 +136,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi"), MangaTracker, AnimeTracker } override suspend fun refresh(track: MangaTrack): MangaTrack { - val remoteStatusTrack = api.statusLibManga(track) - track.copyPersonalFrom(remoteStatusTrack!!) + val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga") + track.copyPersonalFrom(remoteStatusTrack) api.findLibManga(track)?.let { remoteTrack -> track.total_chapters = remoteTrack.total_chapters } @@ -144,8 +145,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi"), MangaTracker, AnimeTracker } override suspend fun refresh(track: AnimeTrack): AnimeTrack { - val remoteStatusTrack = api.statusLibAnime(track) - track.copyPersonalFrom(remoteStatusTrack!!) + val remoteStatusTrack = api.statusLibAnime(track) ?: throw Exception("Could not find anime") + track.copyPersonalFrom(remoteStatusTrack) api.findLibAnime(track)?.let { remoteTrack -> track.total_episodes = remoteTrack.total_episodes } @@ -198,19 +199,19 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi"), MangaTracker, AnimeTracker try { val oauth = api.accessToken(code) interceptor.newAuth(oauth) - saveCredentials(oauth.user_id.toString(), oauth.access_token) + saveCredentials(oauth.userId.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: BGMOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): BGMOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index e2623dcc66..9d6ee49854 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -4,6 +4,10 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import eu.kanade.tachiyomi.network.GET @@ -11,14 +15,6 @@ import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.OkHttpClient @@ -42,9 +38,9 @@ class BangumiApi( return withIOContext { val body = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body)) + authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body)) .awaitSuccess() track } @@ -54,9 +50,9 @@ class BangumiApi( return withIOContext { val body = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body)) + authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body)) .awaitSuccess() track } @@ -67,9 +63,9 @@ class BangumiApi( // read status update val sbody = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody)) + authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody)) .awaitSuccess() // chapter update @@ -77,10 +73,7 @@ class BangumiApi( .add("watched_eps", track.last_chapter_read.toInt().toString()) .build() authClient.newCall( - POST( - "$apiUrl/subject/${track.remote_id}/update/watched_eps", - body = body, - ), + POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body), ).awaitSuccess() track @@ -92,9 +85,9 @@ class BangumiApi( // read status update val sbody = FormBody.Builder() .add("rating", track.score.toInt().toString()) - .add("status", track.toBangumiStatus()) + .add("status", track.toApiStatus()) .build() - authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody)) + authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody)) .awaitSuccess() // chapter update @@ -102,10 +95,7 @@ class BangumiApi( .add("watched_eps", track.last_episode_seen.toInt().toString()) .build() authClient.newCall( - POST( - "$apiUrl/subject/${track.remote_id}/update/watched_eps", - body = body, - ), + POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body), ).awaitSuccess() track @@ -114,114 +104,65 @@ class BangumiApi( suspend fun search(search: String): List { return withIOContext { - val url = "$apiUrl/search/subject/${URLEncoder.encode( + val url = "$API_URL/search/subject/${URLEncoder.encode( search, StandardCharsets.UTF_8.name(), )}" .toUri() .buildUpon() + .appendQueryParameter("type", "1") + .appendQueryParameter("responseGroup", "large") .appendQueryParameter("max_results", "20") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .use { - var responseBody = it.body.string() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":404")) { - responseBody = "{\"results\":0,\"list\":[]}" + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { result -> + if (result.code == 404) emptyList() + + result.list + ?.map { it.toMangaTrackSearch(trackId) } + .orEmpty() } - val response = json.decodeFromString(responseBody)["list"]?.jsonArray - response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } - ?.map { jsonToSearch(it.jsonObject) }.orEmpty() - } + } } } suspend fun searchAnime(search: String): List { return withIOContext { - val url = "$apiUrl/search/subject/${URLEncoder.encode( + val url = "$API_URL/search/subject/${URLEncoder.encode( search, StandardCharsets.UTF_8.name(), )}" .toUri() .buildUpon() + .appendQueryParameter("type", "2") + .appendQueryParameter("responseGroup", "large") .appendQueryParameter("max_results", "20") .build() - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .use { - var responseBody = it.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":404")) { - responseBody = "{\"results\":0,\"list\":[]}" - } - val response = json.decodeFromString(responseBody)["list"]?.jsonArray - response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 } - ?.map { jsonToSearchAnime(it.jsonObject) }.orEmpty() - } - } - } - - private fun jsonToSearch(obj: JsonObject): MangaTrackSearch { - val coverUrl = if (obj["images"] is JsonObject) { - obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: "" - } else { - // Sometimes JsonNull - "" - } - val totalChapters = if (obj["eps_count"] != null) { - obj["eps_count"]!!.jsonPrimitive.long - } else { - 0 - } - val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0 - return MangaTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name_cn"]!!.jsonPrimitive.content - cover_url = coverUrl - summary = obj["name"]!!.jsonPrimitive.content - score = rating - tracking_url = obj["url"]!!.jsonPrimitive.content - total_chapters = totalChapters - } - } - - private fun jsonToSearchAnime(obj: JsonObject): AnimeTrackSearch { - val coverUrl = if (obj["images"] is JsonObject) { - obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: "" - } else { - // Sometimes JsonNull - "" - } - val totalChapters = if (obj["eps_count"] != null) { - obj["eps_count"]!!.jsonPrimitive.long - } else { - 0 - } + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .let { result -> + if (result.code == 404) emptyList() - val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0 - return AnimeTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name_cn"]!!.jsonPrimitive.content - cover_url = coverUrl - score = rating - summary = obj["name"]!!.jsonPrimitive.content - tracking_url = obj["url"]!!.jsonPrimitive.content - total_episodes = totalChapters + result.list + ?.map { it.toAnimeTrackSearch(trackId) } + .orEmpty() + } + } } } suspend fun findLibManga(track: MangaTrack): MangaTrack? { return withIOContext { with(json) { - authClient.newCall(GET("$apiUrl/subject/${track.remote_id}")) + authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) .awaitSuccess() - .parseAs() - .let { jsonToSearch(it) } + .parseAs() + .toMangaTrackSearch(trackId) } } } @@ -229,17 +170,17 @@ class BangumiApi( suspend fun findLibAnime(track: AnimeTrack): AnimeTrack? { return withIOContext { with(json) { - authClient.newCall(GET("$apiUrl/subject/${track.remote_id}")) + authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) .awaitSuccess() - .parseAs() - .let { jsonToSearchAnime(it) } + .parseAs() + .toAnimeTrackSearch(trackId) } } } suspend fun statusLibManga(track: MangaTrack): MangaTrack? { return withIOContext { - val urlUserRead = "$apiUrl/collection/${track.remote_id}" + val urlUserRead = "$API_URL/collection/${track.remote_id}" val requestUserRead = Request.Builder() .url(urlUserRead) .cacheControl(CacheControl.FORCE_NETWORK) @@ -247,27 +188,25 @@ class BangumiApi( .build() // TODO: get user readed chapter here - val response = authClient.newCall(requestUserRead).awaitSuccess() - val responseBody = response.body.string() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":400")) { - null - } else { - json.decodeFromString(responseBody).let { - track.status = it.status?.id!! - track.last_chapter_read = it.ep_status!!.toDouble() - track.score = it.rating!! - track - } + with(json) { + authClient.newCall(requestUserRead) + .awaitSuccess() + .parseAs() + .let { + if (it.code == 400) return@let null + + track.status = it.status?.id!! + track.last_chapter_read = it.epStatus!!.toDouble() + track.score = it.rating!! + track + } } } } suspend fun statusLibAnime(track: AnimeTrack): AnimeTrack? { return withIOContext { - val urlUserRead = "$apiUrl/collection/${track.remote_id}" + val urlUserRead = "$API_URL/collection/${track.remote_id}" val requestUserRead = Request.Builder() .url(urlUserRead) .cacheControl(CacheControl.FORCE_NETWORK) @@ -275,25 +214,23 @@ class BangumiApi( .build() // TODO: get user readed chapter here - var response = authClient.newCall(requestUserRead).awaitSuccess() - var responseBody = response.body?.string().orEmpty() - if (responseBody.isEmpty()) { - throw Exception("Null Response") - } - if (responseBody.contains("\"code\":400")) { - null - } else { - json.decodeFromString(responseBody).let { - track.status = it.status?.id!! - track.last_episode_seen = it.ep_status!!.toDouble() - track.score = it.rating!! - track - } + with(json) { + authClient.newCall(requestUserRead) + .awaitSuccess() + .parseAs() + .let { + if (it.code == 400) return@let null + + track.status = it.status?.id!! + track.last_episode_seen = it.epStatus!!.toDouble() + track.score = it.rating!! + track + } } } } - suspend fun accessToken(code: String): OAuth { + suspend fun accessToken(code: String): BGMOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) @@ -304,41 +241,41 @@ class BangumiApi( } private fun accessTokenRequest(code: String) = POST( - oauthUrl, + OAUTH_URL, body = FormBody.Builder() .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .add("code", code) - .add("redirect_uri", redirectUrl) + .add("redirect_uri", REDIRECT_URL) .build(), ) companion object { - private const val clientId = "bgm293165b66d7e58156" - private const val clientSecret = "21d5f5c19ac24b4bc9c855ffa2387030" + private const val CLIENT_ID = "bgm293165b66d7e58156" + private const val CLIENT_SECRET = "21d5f5c19ac24b4bc9c855ffa2387030" - private const val apiUrl = "https://api.bgm.tv" - private const val oauthUrl = "https://bgm.tv/oauth/access_token" - private const val loginUrl = "https://bgm.tv/oauth/authorize" + private const val API_URL = "https://api.bgm.tv" + private const val OAUTH_URL = "https://bgm.tv/oauth/access_token" + private const val LOGIN_URL = "https://bgm.tv/oauth/authorize" - private const val redirectUrl = "animetail://bangumi-auth" + private const val REDIRECT_URL = "animetail://bangumi-auth" fun authUrl(): Uri = - loginUrl.toUri().buildUpon() - .appendQueryParameter("client_id", clientId) + LOGIN_URL.toUri().buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("response_type", "code") - .appendQueryParameter("redirect_uri", redirectUrl) + .appendQueryParameter("redirect_uri", REDIRECT_URL) .build() fun refreshTokenRequest(token: String) = POST( - oauthUrl, + OAUTH_URL, body = FormBody.Builder() .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .add("refresh_token", token) - .add("redirect_uri", redirectUrl) + .add("redirect_uri", REDIRECT_URL) .build(), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt index e707802997..fec6f75080 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.bangumi import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth +import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.FormBody import okhttp3.Interceptor @@ -14,7 +16,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = bangumi.restoreToken() + private var oauth: BGMOAuth? = bangumi.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -22,9 +24,9 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi") if (currAuth.isExpired()) { - val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!)) + val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!)) if (response.isSuccessful) { - newAuth(json.decodeFromString(response.body.string())) + newAuth(json.decodeFromString(response.body.string())) } else { response.close() } @@ -38,28 +40,28 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor { .apply { if (originalRequest.method == "GET") { val newUrl = originalRequest.url.newBuilder() - .addQueryParameter("access_token", currAuth.access_token) + .addQueryParameter("access_token", currAuth.accessToken) .build() url(newUrl) } else { - post(addToken(currAuth.access_token, originalRequest.body as FormBody)) + post(addToken(currAuth.accessToken, originalRequest.body as FormBody)) } } .build() .let(chain::proceed) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: BGMOAuth?) { this.oauth = if (oauth == null) { null } else { - OAuth( - oauth.access_token, - oauth.token_type, + BGMOAuth( + oauth.accessToken, + oauth.tokenType, System.currentTimeMillis() / 1000, - oauth.expires_in, - oauth.refresh_token, - this.oauth?.user_id, + oauth.expiresIn, + oauth.refreshToken, + this.oauth?.userId, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt deleted file mode 100644 index 99521fd164..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt +++ /dev/null @@ -1,74 +0,0 @@ -package eu.kanade.tachiyomi.data.track.bangumi - -import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack -import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack -import kotlinx.serialization.Serializable - -@Serializable -data class Avatar( - val large: String? = "", - val medium: String? = "", - val small: String? = "", -) - -@Serializable -data class Collection( - val `private`: Int? = 0, - val comment: String? = "", - val ep_status: Int? = 0, - val lasttouch: Int? = 0, - val rating: Double? = 0.0, - val status: Status? = Status(), - val tag: List? = emptyList(), - val user: User? = User(), - val vol_status: Int? = 0, -) - -@Serializable -data class Status( - val id: Long? = 0, - val name: String? = "", - val type: String? = "", -) - -@Serializable -data class User( - val avatar: Avatar? = Avatar(), - val id: Int? = 0, - val nickname: String? = "", - val sign: String? = "", - val url: String? = "", - val usergroup: Int? = 0, - val username: String? = "", -) - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long = System.currentTimeMillis() / 1000, - val expires_in: Long, - val refresh_token: String?, - val user_id: Long?, -) - -// Access token refresh before expired -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -fun MangaTrack.toBangumiStatus() = when (status) { - Bangumi.READING -> "do" - Bangumi.COMPLETED -> "collect" - Bangumi.ON_HOLD -> "on_hold" - Bangumi.DROPPED -> "dropped" - Bangumi.PLAN_TO_READ -> "wish" - else -> throw NotImplementedError("Unknown status: $status") -} - -fun AnimeTrack.toBangumiStatus() = when (status) { - Bangumi.READING -> "do" - Bangumi.COMPLETED -> "collect" - Bangumi.ON_HOLD -> "on_hold" - Bangumi.DROPPED -> "dropped" - Bangumi.PLAN_TO_READ -> "wish" - else -> throw NotImplementedError("Unknown status: $status") -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt new file mode 100644 index 0000000000..32b0029193 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiUtils.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.data.track.bangumi + +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack + +fun MangaTrack.toApiStatus() = when (status) { + Bangumi.READING -> "do" + Bangumi.COMPLETED -> "collect" + Bangumi.ON_HOLD -> "on_hold" + Bangumi.DROPPED -> "dropped" + Bangumi.PLAN_TO_READ -> "wish" + else -> throw NotImplementedError("Unknown status: $status") +} + +fun AnimeTrack.toApiStatus() = when (status) { + Bangumi.READING -> "do" + Bangumi.COMPLETED -> "collect" + Bangumi.ON_HOLD -> "on_hold" + Bangumi.DROPPED -> "dropped" + Bangumi.PLAN_TO_READ -> "wish" + else -> throw NotImplementedError("Unknown status: $status") +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt new file mode 100644 index 0000000000..85501934f7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMCollectionResponse.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BGMCollectionResponse( + val code: Int?, + val `private`: Int? = 0, + val comment: String? = "", + @SerialName("ep_status") + val epStatus: Int? = 0, + @SerialName("lasttouch") + val lastTouch: Int? = 0, + val rating: Double? = 0.0, + val status: Status? = Status(), + val tag: List? = emptyList(), + val user: User? = User(), + @SerialName("vol_status") + val volStatus: Int? = 0, +) + +@Serializable +data class Status( + val id: Long? = 0, + val name: String? = "", + val type: String? = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt new file mode 100644 index 0000000000..6a4fea3cb9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMOAuth.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BGMOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + val createdAt: Long = System.currentTimeMillis() / 1000, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, + @SerialName("user_id") + val userId: Long?, +) + +// Access token refresh before expired +fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt new file mode 100644 index 0000000000..fc91670899 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt @@ -0,0 +1,65 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BGMSearchResult( + val list: List?, + val code: Int?, +) + +@Serializable +data class BGMSearchItem( + val id: Long, + @SerialName("name_cn") + val nameCn: String, + val name: String, + val type: Int, + val summary: String?, + val images: BGMSearchItemCovers?, + @SerialName("eps_count") + val epsCount: Long?, + val rating: BGMSearchItemRating?, + val url: String, +) { + fun toMangaTrackSearch(trackId: Long): MangaTrackSearch = MangaTrackSearch.create(trackId).apply { + remote_id = this@BGMSearchItem.id + title = nameCn.ifBlank { name } + cover_url = images?.common.orEmpty() + summary = if (nameCn.isNotBlank()) { + "作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty() + } else { + this@BGMSearchItem.summary.orEmpty() + } + score = rating?.score ?: -1.0 + tracking_url = url + total_chapters = epsCount ?: 0 + } + + fun toAnimeTrackSearch(trackId: Long): AnimeTrackSearch = AnimeTrackSearch.create(trackId).apply { + remote_id = this@BGMSearchItem.id + title = nameCn.ifBlank { name } + cover_url = images?.common.orEmpty() + summary = if (nameCn.isNotBlank()) { + "作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty() + } else { + this@BGMSearchItem.summary.orEmpty() + } + score = rating?.score ?: -1.0 + tracking_url = url + total_episodes = epsCount ?: 0 + } +} + +@Serializable +data class BGMSearchItemCovers( + val common: String?, +) + +@Serializable +data class BGMSearchItemRating( + val score: Double?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt new file mode 100644 index 0000000000..375c39eb64 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMUser.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.bangumi.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Avatar( + val large: String? = "", + val medium: String? = "", + val small: String? = "", +) + +@Serializable +data class User( + val avatar: Avatar? = Avatar(), + val id: Int? = 0, + val nickname: String? = "", + val sign: String? = "", + val url: String? = "", + @SerialName("usergroup") + val userGroup: Int? = 0, + val username: String? = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinApi.kt index 9ef97719db..486ae6bcd9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinApi.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.jellyfin import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.track.jellyfin.dto.JFItem +import eu.kanade.tachiyomi.data.track.jellyfin.dto.JFItemList import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -34,7 +36,7 @@ class JellyfinApi( val track = with(json) { client.newCall(GET(url)) .awaitSuccess() - .parseAs() + .parseAs() .toTrack() }.apply { tracking_url = url } @@ -50,7 +52,7 @@ class JellyfinApi( } } - private fun ItemDto.toTrack(): AnimeTrackSearch = AnimeTrackSearch.create( + private fun JFItem.toTrack(): AnimeTrackSearch = AnimeTrackSearch.create( trackId, ).also { it.title = name @@ -87,7 +89,7 @@ class JellyfinApi( val episodes = with(json) { client.newCall(GET(episodesUrl)) .awaitSuccess() - .parseAs() + .parseAs() }.items val totalEpisodes = episodes.last().indexNumber!! @@ -129,7 +131,7 @@ class JellyfinApi( val episodes = with(json) { client.newCall(GET(episodesUrl)) .awaitSuccess() - .parseAs() + .parseAs() }.items episodes.firstOrNull { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinInterceptor.kt index a612fd800a..ca420f3ef8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinInterceptor.kt @@ -41,7 +41,6 @@ class JellyfinInterceptor : Interceptor { return chain.proceed(authRequest) } - @Suppress("MagicNumber") private fun getId(suffix: Int): Long { val key = "jellyfin" + (if (suffix == 1) "" else " ($suffix)") + "/all/$JELLYFIN_VERSION_ID" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/dto/JFItem.kt similarity index 59% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/dto/JFItem.kt index 2f80e864d0..3526a38cd7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/JellyfinModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/jellyfin/dto/JFItem.kt @@ -1,22 +1,22 @@ -package eu.kanade.tachiyomi.data.track.jellyfin +package eu.kanade.tachiyomi.data.track.jellyfin.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ItemDto( +data class JFItem( @SerialName("Name") val name: String, @SerialName("Id") val id: String, - @SerialName("UserData") val userData: UserDataDto, + @SerialName("UserData") val userData: JFUserData, @SerialName("IndexNumber") val indexNumber: Long? = null, ) @Serializable -data class UserDataDto( +data class JFUserData( @SerialName("Played") val played: Boolean, ) @Serializable -data class ItemsDto( - @SerialName("Items") val items: List, +data class JFItemList( + @SerialName("Items") val items: List, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 977d524169..88bae6695f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableAnimeTracker import eu.kanade.tachiyomi.data.track.DeletableMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import kotlinx.collections.immutable.ImmutableList @@ -235,13 +236,13 @@ class Kitsu(id: Long) : return getPassword() } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: KitsuOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): KitsuOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index e4bb52397a..b2fb519700 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -3,6 +3,12 @@ package eu.kanade.tachiyomi.data.track.kitsu import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddEntryResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAlgoliaSearchResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import eu.kanade.tachiyomi.network.DELETE @@ -12,12 +18,8 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.FormBody @@ -46,7 +48,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) putJsonObject("data") { put("type", "libraryEntries") putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_chapter_read.toInt()) } putJsonObject("relationships") { @@ -69,19 +71,15 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) with(json) { authClient.newCall( POST( - "${baseUrl}library-entries", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - body = data.toString() - .toRequestBody("application/vnd.api+json".toMediaType()), + "${BASE_URL}library-entries", + headers = headersOf("Content-Type", VND_API_JSON), + body = data.toString().toRequestBody(VND_JSON_MEDIA_TYPE), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long + track.remote_id = it.data.id track } } @@ -94,7 +92,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) putJsonObject("data") { put("type", "libraryEntries") putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_episode_seen.toInt()) } putJsonObject("relationships") { @@ -117,19 +115,15 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) with(json) { authClient.newCall( POST( - "${baseUrl}library-entries", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - body = data.toString() - .toRequestBody("application/vnd.api+json".toMediaType()), + "${BASE_URL}library-entries", + headers = headersOf("Content-Type", VND_API_JSON), + body = data.toString().toRequestBody(VND_JSON_MEDIA_TYPE), ), ) .awaitSuccess() - .parseAs() + .parseAs() .let { - track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long + track.remote_id = it.data.id track } } @@ -143,36 +137,27 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) put("type", "libraryEntries") put("id", track.remote_id) putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_chapter_read.toInt()) - put("ratingTwenty", track.toKitsuScore()) + put("ratingTwenty", track.toApiScore()) put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) } } } - with(json) { - authClient.newCall( - Request.Builder() - .url("${baseUrl}library-entries/${track.remote_id}") - .headers( - headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ) - .patch( - data.toString().toRequestBody("application/vnd.api+json".toMediaType()), - ) - .build(), - ) - .awaitSuccess() - .parseAs() - .let { - track - } - } + authClient.newCall( + Request.Builder() + .url("${BASE_URL}library-entries/${track.remote_id}") + .headers( + headersOf("Content-Type", VND_API_JSON), + ) + .patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE)) + .build(), + ) + .awaitSuccess() + + track } } @@ -183,67 +168,50 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) put("type", "libraryEntries") put("id", track.remote_id) putJsonObject("attributes") { - put("status", track.toKitsuStatus()) + put("status", track.toApiStatus()) put("progress", track.last_episode_seen.toInt()) - put("ratingTwenty", track.toKitsuScore()) + put("ratingTwenty", track.toApiScore()) put("startedAt", KitsuDateHelper.convert(track.started_watching_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_watching_date)) } } } - with(json) { - authClient.newCall( - Request.Builder() - .url("${baseUrl}library-entries/${track.remote_id}") - .headers( - headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ) - .patch( - data.toString().toRequestBody("application/vnd.api+json".toMediaType()), - ) - .build(), - ) - .awaitSuccess() - .parseAs() - .let { - track - } - } + authClient.newCall( + Request.Builder() + .url("${BASE_URL}library-entries/${track.remote_id}") + .headers( + headersOf("Content-Type", VND_API_JSON), + ) + .patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE)) + .build(), + ) + .awaitSuccess() + + track } } suspend fun removeLibManga(track: DomainMangaTrack) { withIOContext { - authClient - .newCall( - DELETE( - "${baseUrl}library-entries/${track.remoteId}", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ), - ) + authClient.newCall( + DELETE( + "${BASE_URL}library-entries/${track.remoteId}", + headers = headersOf("Content-Type", VND_API_JSON), + ), + ) .awaitSuccess() } } suspend fun removeLibAnime(track: DomainAnimeTrack) { withIOContext { - authClient - .newCall( - DELETE( - "${baseUrl}library-entries/${track.remoteId}", - headers = headersOf( - "Content-Type", - "application/vnd.api+json", - ), - ), - ) + authClient.newCall( + DELETE( + "${BASE_URL}library-entries/${track.remoteId}", + headers = headersOf("Content-Type", VND_API_JSON), + ), + ) .awaitSuccess() } } @@ -251,12 +219,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun search(query: String): List { return withIOContext { with(json) { - authClient.newCall(GET(algoliaKeyUrl)) + authClient.newCall(GET(ALGOLIA_KEY_URL)) .awaitSuccess() - .parseAs() + .parseAs() .let { - val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content - algoliaSearch(key, query) + algoliaSearch(it.media.key, query) } } } @@ -265,12 +232,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun searchAnime(query: String): List { return withIOContext { with(json) { - authClient.newCall(GET(algoliaKeyUrl)) + authClient.newCall(GET(ALGOLIA_KEY_URL)) .awaitSuccess() - .parseAs() + .parseAs() .let { - val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content - algoliaSearchAnime(key, query) + algoliaSearchAnime(it.media.key, query) } } } @@ -281,17 +247,17 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val jsonObject = buildJsonObject { put( "params", - "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter", + "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$ALGOLIA_FILTER", ) } with(json) { client.newCall( POST( - algoliaUrl, + ALGOLIA_URL, headers = headersOf( "X-Algolia-Application-Id", - algoliaAppId, + ALGOLIA_APP_ID, "X-Algolia-API-Key", key, ), @@ -299,13 +265,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ), ) .awaitSuccess() - .parseAs() - .let { - it["hits"]!!.jsonArray - .map { KitsuSearchManga(it.jsonObject) } - .filter { it.subType != "novel" } - .map { it.toTrack() } - } + .parseAs() + .hits + .filter { it.subtype != "novel" } + .map { it.toMangaTrack() } } } } @@ -315,17 +278,17 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) val jsonObject = buildJsonObject { put( "params", - "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilterAnime", + "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$ALGOLIA_FILTER_ANIME", ) } with(json) { client.newCall( POST( - algoliaUrl, + ALGOLIA_URL, headers = headersOf( "X-Algolia-Application-Id", - algoliaAppId, + ALGOLIA_APP_ID, "X-Algolia-API-Key", key, ), @@ -333,32 +296,27 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) ), ) .awaitSuccess() - .parseAs() - .let { - it["hits"]!!.jsonArray - .map { KitsuSearchAnime(it.jsonObject) } - .filter { it.subType != "novel" } - .map { it.toTrack() } - } + .parseAs() + .hits + .filter { it.subtype != "novel" } + .map { it.toAnimeTrack() } } } } suspend fun findLibManga(track: MangaTrack, userId: String): MangaTrack? { return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() + val url = "${BASE_URL}library-entries".toUri().buildUpon() .encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId") .appendQueryParameter("include", "manga") .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToMangaTrack() } else { null } @@ -369,19 +327,17 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun findLibAnime(track: AnimeTrack, userId: String): AnimeTrack? { return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() + val url = "${BASE_URL}library-entries".toUri().buildUpon() .encodedQuery("filter[anime_id]=${track.remote_id}&filter[user_id]=$userId") .appendQueryParameter("include", "anime") .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val anime = it["included"]!!.jsonArray[0].jsonObject - KitsuLibAnime(data[0].jsonObject, anime).toTrack() + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToAnimeTrack() } else { null } @@ -392,19 +348,17 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun getLibManga(track: MangaTrack): MangaTrack { return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() + val url = "${BASE_URL}library-entries".toUri().buildUpon() .encodedQuery("filter[id]=${track.remote_id}") .appendQueryParameter("include", "manga") .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val manga = it["included"]!!.jsonArray[0].jsonObject - KitsuLibManga(data[0].jsonObject, manga).toTrack() + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToMangaTrack() } else { throw Exception("Could not find manga") } @@ -415,38 +369,36 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun getLibAnime(track: AnimeTrack): AnimeTrack { return withIOContext { - val url = "${baseUrl}library-entries".toUri().buildUpon() + val url = "${BASE_URL}library-entries".toUri().buildUpon() .encodedQuery("filter[id]=${track.remote_id}") .appendQueryParameter("include", "anime") .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val data = it["data"]!!.jsonArray - if (data.size > 0) { - val anime = it["included"]!!.jsonArray[0].jsonObject - KitsuLibAnime(data[0].jsonObject, anime).toTrack() + if (it.data.isNotEmpty() && it.included.isNotEmpty()) { + it.firstToAnimeTrack() } else { - throw Exception("Could not find anime") + throw Exception("Could not find manga") } } } } } - suspend fun login(username: String, password: String): OAuth { + suspend fun login(username: String, password: String): KitsuOAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() .add("username", username) .add("password", password) .add("grant_type", "password") - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .build() with(json) { - client.newCall(POST(loginUrl, body = formBody)) + client.newCall(POST(LOGIN_URL, body = formBody)) .awaitSuccess() .parseAs() } @@ -455,59 +407,61 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) suspend fun getCurrentUser(): String { return withIOContext { - val url = "${baseUrl}users".toUri().buildUpon() + val url = "${BASE_URL}users".toUri().buildUpon() .encodedQuery("filter[self]=true") .build() with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content - } + .parseAs() + .data[0] + .id } } } companion object { - private const val clientId = + private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" - private const val clientSecret = + private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" - private const val baseUrl = "https://kitsu.io/api/edge/" - private const val loginUrl = "https://kitsu.io/api/oauth/token" - private const val baseMangaUrl = "https://kitsu.io/manga/" - private const val baseAnimeUrl = "https://kitsu.io/anime/" - private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/" + private const val BASE_URL = "https://kitsu.app/api/edge/" + private const val LOGIN_URL = "https://kitsu.app/api/oauth/token" + private const val BASE_MANGA_URL = "https://kitsu.app/manga/" + private const val BASE_ANIME_URL = "https://kitsu.app/anime/" + private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/" - private const val algoliaUrl = + private const val ALGOLIA_URL = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/" - private const val algoliaAppId = "AWQO5J657S" - private const val algoliaFilter = + private const val ALGOLIA_APP_ID = "AWQO5J657S" + private const val ALGOLIA_FILTER = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" + "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" - private const val algoliaFilterAnime = + private const val ALGOLIA_FILTER_ANIME = "&facetFilters=%5B%22kind%3Aanime%22%5D&attributesToRetrieve=" + "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22episodeCount%22%2C%22" + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" + private const val VND_API_JSON = "application/vnd.api+json" + private val VND_JSON_MEDIA_TYPE = VND_API_JSON.toMediaType() + fun mangaUrl(remoteId: Long): String { - return baseMangaUrl + remoteId + return BASE_MANGA_URL + remoteId } fun animeUrl(remoteId: Long): String { - return baseAnimeUrl + remoteId + return BASE_ANIME_URL + remoteId } fun refreshTokenRequest(token: String) = POST( - loginUrl, + LOGIN_URL, body = FormBody.Builder() .add("grant_type", "refresh_token") .add("refresh_token", token) - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .build(), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt index 6828e1e1a9..e4521438b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuDateHelper.kt @@ -6,8 +6,8 @@ import java.util.Locale object KitsuDateHelper { - private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH) + private const val PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + private val formatter = SimpleDateFormat(PATTERN, Locale.ENGLISH) fun convert(dateValue: Long): String? { if (dateValue == 0L) return null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt index 52e70a0118..119e1d0522 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.kitsu import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth +import eu.kanade.tachiyomi.data.track.kitsu.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response @@ -13,14 +15,14 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = kitsu.restoreToken() + private var oauth: KitsuOAuth? = kitsu.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu") - val refreshToken = currAuth.refresh_token!! + val refreshToken = currAuth.refreshToken!! // Refresh access token if expired. if (currAuth.isExpired()) { @@ -34,7 +36,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Animetail v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .header("Accept", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json") @@ -43,7 +45,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor { return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: KitsuOAuth?) { this.oauth = oauth kitsu.saveToken(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt deleted file mode 100644 index b1de71a4d5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ /dev/null @@ -1,216 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu - -import androidx.annotation.CallSuper -import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack -import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack -import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch -import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -class KitsuSearchManga(obj: JsonObject) { - val id = obj["id"]!!.jsonPrimitive.long - private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content - private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.longOrNull - val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull - val original = try { - obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content - } catch (e: IllegalArgumentException) { - // posterImage is sometimes a jsonNull object instead - null - } - private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull - private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull() - private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it.toLong() * 1000)) - } - private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull - - @CallSuper - fun toTrack() = MangaTrackSearch.create(TrackerManager.KITSU).apply { - remote_id = this@KitsuSearchManga.id - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original ?: "" - summary = synopsis ?: "" - tracking_url = KitsuApi.mangaUrl(remote_id) - score = rating ?: -1.0 - publishing_status = if (endDate == null) { - "Publishing" - } else { - "Finished" - } - publishing_type = subType ?: "" - start_date = startDate ?: "" - } -} - -class KitsuSearchAnime(obj: JsonObject) { - val id = obj["id"]!!.jsonPrimitive.long - private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content - private val episodeCount = obj["episodeCount"]?.jsonPrimitive?.longOrNull - val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull - val original = try { - obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content - } catch (e: IllegalArgumentException) { - // posterImage is sometimes a jsonNull object instead - null - } - private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull - private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull() - private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it.toLong() * 1000)) - } - private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull - - @CallSuper - fun toTrack() = AnimeTrackSearch.create(TrackerManager.KITSU).apply { - remote_id = this@KitsuSearchAnime.id - title = canonicalTitle - total_episodes = episodeCount ?: 0 - cover_url = original ?: "" - summary = synopsis ?: "" - tracking_url = KitsuApi.animeUrl(remote_id) - score = rating ?: -1.0 - publishing_status = if (endDate == null) { - "Publishing" - } else { - "Finished" - } - publishing_type = subType ?: "" - start_date = startDate ?: "" - } -} - -class KitsuLibManga(obj: JsonObject, manga: JsonObject) { - val id = manga["id"]!!.jsonPrimitive.int - private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content - private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.longOrNull - val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty() - val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content - private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content - private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty() - private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull - private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull - private val libraryId = obj["id"]!!.jsonPrimitive.long - val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content - private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull - val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int - - fun toTrack() = MangaTrackSearch.create(TrackerManager.KITSU).apply { - remote_id = libraryId - title = canonicalTitle - total_chapters = chapterCount ?: 0 - cover_url = original - summary = synopsis - tracking_url = KitsuApi.mangaUrl(remote_id) - publishing_status = this@KitsuLibManga.status - publishing_type = type - start_date = startDate - started_reading_date = KitsuDateHelper.parse(startedAt) - finished_reading_date = KitsuDateHelper.parse(finishedAt) - status = toTrackStatus() - score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 - last_chapter_read = progress.toDouble() - } - - private fun toTrackStatus() = when (status) { - "current" -> Kitsu.READING - "completed" -> Kitsu.COMPLETED - "on_hold" -> Kitsu.ON_HOLD - "dropped" -> Kitsu.DROPPED - "planned" -> Kitsu.PLAN_TO_READ - else -> throw Exception("Unknown status") - } -} - -class KitsuLibAnime(obj: JsonObject, anime: JsonObject) { - val id = anime["id"]!!.jsonPrimitive.int - private val canonicalTitle = anime["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content - private val episodeCount = anime["attributes"]!!.jsonObject["episodeCount"]?.jsonPrimitive?.longOrNull - val type = anime["attributes"]!!.jsonObject["subtype"]?.jsonPrimitive?.contentOrNull.orEmpty() - val original = anime["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content - private val synopsis = anime["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content - private val startDate = anime["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty() - private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull - private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull - private val libraryId = obj["id"]!!.jsonPrimitive.long - val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content - private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull - val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int - - fun toTrack() = AnimeTrackSearch.create(TrackerManager.KITSU).apply { - remote_id = libraryId - title = canonicalTitle - total_episodes = episodeCount ?: 0 - cover_url = original - summary = synopsis - tracking_url = KitsuApi.animeUrl(remote_id) - publishing_status = this@KitsuLibAnime.status - publishing_type = type - start_date = startDate - started_watching_date = KitsuDateHelper.parse(startedAt) - finished_watching_date = KitsuDateHelper.parse(finishedAt) - status = toTrackStatus() - score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0 - last_episode_seen = progress.toDouble() - } - - private fun toTrackStatus() = when (status) { - "current" -> Kitsu.WATCHING - "completed" -> Kitsu.COMPLETED - "on_hold" -> Kitsu.ON_HOLD - "dropped" -> Kitsu.DROPPED - "planned" -> Kitsu.PLAN_TO_WATCH - else -> throw Exception("Unknown status") - } -} - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, -) - -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) - -fun MangaTrack.toKitsuStatus() = when (status) { - Kitsu.READING -> "current" - Kitsu.COMPLETED -> "completed" - Kitsu.ON_HOLD -> "on_hold" - Kitsu.DROPPED -> "dropped" - Kitsu.PLAN_TO_READ -> "planned" - else -> throw Exception("Unknown status") -} - -fun MangaTrack.toKitsuScore(): String? { - return if (score > 0) (score * 2).toInt().toString() else null -} - -fun AnimeTrack.toKitsuStatus() = when (status) { - Kitsu.WATCHING -> "current" - Kitsu.COMPLETED -> "completed" - Kitsu.ON_HOLD -> "on_hold" - Kitsu.DROPPED -> "dropped" - Kitsu.PLAN_TO_WATCH -> "planned" - else -> throw Exception("Unknown status") -} - -fun AnimeTrack.toKitsuScore(): String? { - return if (score > 0) (score * 2).toInt().toString() else null -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt new file mode 100644 index 0000000000..d7638ea956 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.data.track.kitsu + +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack + +fun MangaTrack.toApiStatus() = when (status) { + Kitsu.READING -> "current" + Kitsu.COMPLETED -> "completed" + Kitsu.ON_HOLD -> "on_hold" + Kitsu.DROPPED -> "dropped" + Kitsu.PLAN_TO_READ -> "planned" + else -> throw Exception("Unknown status") +} + +fun AnimeTrack.toApiStatus() = when (status) { + Kitsu.WATCHING -> "current" + Kitsu.COMPLETED -> "completed" + Kitsu.ON_HOLD -> "on_hold" + Kitsu.DROPPED -> "dropped" + Kitsu.PLAN_TO_WATCH -> "planned" + else -> throw Exception("Unknown status") +} + +fun MangaTrack.toApiScore(): String? { + return if (score > 0) (score * 2).toInt().toString() else null +} + +fun AnimeTrack.toApiScore(): String? { + return if (score > 0) (score * 2).toInt().toString() else null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddEntry.kt new file mode 100644 index 0000000000..d852e356f1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddEntry.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuAddEntryResult( + val data: KitsuAddEntryItem, +) + +@Serializable +data class KitsuAddEntryItem( + val id: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt new file mode 100644 index 0000000000..eef1efe3c6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt @@ -0,0 +1,115 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.kitsu.Kitsu +import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi +import eu.kanade.tachiyomi.data.track.kitsu.KitsuDateHelper +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuListSearchResult( + val data: List, + val included: List = emptyList(), +) { + fun firstToMangaTrack(): MangaTrackSearch { + require(data.isNotEmpty()) { "Missing User data from Kitsu" } + require(included.isNotEmpty()) { "Missing Manga data from Kitsu" } + + val userData = data[0] + val userDataAttrs = userData.attributes + val manga = included[0].attributes + + return MangaTrackSearch.create(TrackerManager.KITSU).apply { + remote_id = userData.id + title = manga.canonicalTitle + total_chapters = manga.chapterCount ?: 0 + cover_url = manga.posterImage?.original ?: "" + summary = manga.synopsis ?: "" + tracking_url = KitsuApi.mangaUrl(remote_id) + publishing_status = manga.status + publishing_type = manga.mangaType ?: "" + start_date = userDataAttrs.startedAt ?: "" + started_reading_date = KitsuDateHelper.parse(userDataAttrs.startedAt) + finished_reading_date = KitsuDateHelper.parse(userDataAttrs.finishedAt) + status = when (userDataAttrs.status) { + "current" -> Kitsu.READING + "completed" -> Kitsu.COMPLETED + "on_hold" -> Kitsu.ON_HOLD + "dropped" -> Kitsu.DROPPED + "planned" -> Kitsu.PLAN_TO_READ + else -> throw Exception("Unknown status") + } + score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0 + last_chapter_read = userDataAttrs.progress.toDouble() + } + } + + fun firstToAnimeTrack(): AnimeTrackSearch { + require(data.isNotEmpty()) { "Missing User data from Kitsu" } + require(included.isNotEmpty()) { "Missing Manga data from Kitsu" } + + val userData = data[0] + val userDataAttrs = userData.attributes + val anime = included[0].attributes + + return AnimeTrackSearch.create(TrackerManager.KITSU).apply { + remote_id = userData.id + title = anime.canonicalTitle + total_episodes = anime.episodeCount ?: 0 + cover_url = anime.posterImage?.original ?: "" + summary = anime.synopsis ?: "" + tracking_url = KitsuApi.animeUrl(remote_id) + publishing_status = anime.status + publishing_type = anime.showType ?: "" + start_date = userDataAttrs.startedAt ?: "" + started_watching_date = KitsuDateHelper.parse(userDataAttrs.startedAt) + finished_watching_date = KitsuDateHelper.parse(userDataAttrs.finishedAt) + status = when (userDataAttrs.status) { + "current" -> Kitsu.WATCHING + "completed" -> Kitsu.COMPLETED + "on_hold" -> Kitsu.ON_HOLD + "dropped" -> Kitsu.DROPPED + "planned" -> Kitsu.PLAN_TO_WATCH + else -> throw Exception("Unknown status") + } + score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0 + last_episode_seen = userDataAttrs.progress.toDouble() + } + } +} + +@Serializable +data class KitsuListSearchItemData( + val id: Long, + val attributes: KitsuListSearchItemDataAttributes, +) + +@Serializable +data class KitsuListSearchItemDataAttributes( + val status: String, + val startedAt: String?, + val finishedAt: String?, + val ratingTwenty: Int?, + val progress: Int, +) + +@Serializable +data class KitsuListSearchItemIncluded( + val id: Long, + val attributes: KitsuListSearchItemIncludedAttributes, +) + +@Serializable +data class KitsuListSearchItemIncludedAttributes( + val canonicalTitle: String, + val chapterCount: Long?, + val episodeCount: Long?, + val mangaType: String?, + val showType: String?, + val posterImage: KitsuSearchItemCover?, + val synopsis: String?, + val startDate: String?, + val status: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt new file mode 100644 index 0000000000..c5cab234a8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuOAuth.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + val createdAt: Long, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, +) + +fun KitsuOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt new file mode 100644 index 0000000000..ccf36078f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearch.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Serializable +data class KitsuSearchResult( + val media: KitsuSearchResultData, +) + +@Serializable +data class KitsuSearchResultData( + val key: String, +) + +@Serializable +data class KitsuAlgoliaSearchResult( + val hits: List, +) + +@Serializable +data class KitsuAlgoliaSearchItem( + val id: Long, + val canonicalTitle: String, + val chapterCount: Long?, + val episodeCount: Long?, + val subtype: String?, + val posterImage: KitsuSearchItemCover?, + val synopsis: String?, + val averageRating: Double?, + val startDate: Long?, + val endDate: Long?, +) { + fun toMangaTrack(): MangaTrackSearch { + return MangaTrackSearch.create(TrackerManager.KITSU).apply { + remote_id = this@KitsuAlgoliaSearchItem.id + title = canonicalTitle + total_chapters = chapterCount ?: 0 + cover_url = posterImage?.original ?: "" + summary = synopsis ?: "" + tracking_url = KitsuApi.mangaUrl(remote_id) + score = averageRating ?: -1.0 + publishing_status = if (endDate == null) "Publishing" else "Finished" + publishing_type = subtype ?: "" + start_date = startDate?.let { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(Date(it * 1000)) + } ?: "" + } + } + + fun toAnimeTrack(): AnimeTrackSearch { + return AnimeTrackSearch.create(TrackerManager.KITSU).apply { + remote_id = this@KitsuAlgoliaSearchItem.id + title = canonicalTitle + total_episodes = episodeCount ?: 0 + cover_url = posterImage?.original ?: "" + summary = synopsis ?: "" + tracking_url = KitsuApi.animeUrl(remote_id) + score = averageRating ?: -1.0 + publishing_status = if (endDate == null) "Publishing" else "Finished" + publishing_type = subtype ?: "" + start_date = startDate?.let { + val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + outputDf.format(Date(it * 1000)) + } ?: "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt new file mode 100644 index 0000000000..6c062bf527 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchItemCover.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuSearchItemCover( + val original: String?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt new file mode 100644 index 0000000000..d023376511 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.kitsu.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class KitsuCurrentUserResult( + val data: List, +) + +@Serializable +data class KitsuUser( + val id: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt index 32a9c2b975..cac8057b7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch @@ -107,7 +107,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), MangaTracker, De return track.copyFrom(series, rating) } - private fun MangaTrack.copyFrom(item: ListItem, rating: Rating?): MangaTrack = apply { + private fun MangaTrack.copyFrom(item: MUListItem, rating: MURating?): MangaTrack = apply { item.copyTo(this) score = rating?.rating ?: 0.0 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt index a98ec12c77..155d7f486b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.mangaupdates import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating -import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUContext +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MULoginResponse +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURecord +import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUSearchResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -14,21 +16,15 @@ import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.add import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import logcat.LogPriority import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack @@ -38,20 +34,17 @@ class MangaUpdatesApi( ) { private val json: Json by injectLazy() - private val baseUrl = "https://api.mangaupdates.com" - private val contentType = "application/vnd.api+json".toMediaType() - private val authClient by lazy { client.newBuilder() .addInterceptor(interceptor) .build() } - suspend fun getSeriesListItem(track: MangaTrack): Pair { + suspend fun getSeriesListItem(track: MangaTrack): Pair { val listItem = with(json) { - authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}")) + authClient.newCall(GET("$BASE_URL/v1/lists/series/${track.remote_id}")) .awaitSuccess() - .parseAs() + .parseAs() } val rating = getSeriesRating(track) @@ -71,8 +64,8 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() @@ -98,8 +91,8 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series/update", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series/update", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() @@ -113,19 +106,19 @@ class MangaUpdatesApi( } authClient.newCall( POST( - url = "$baseUrl/v1/lists/series/delete", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/lists/series/delete", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() } - private suspend fun getSeriesRating(track: MangaTrack): Rating? { + private suspend fun getSeriesRating(track: MangaTrack): MURating? { return try { with(json) { - authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating")) + authClient.newCall(GET("$BASE_URL/v1/series/${track.remote_id}/rating")) .awaitSuccess() - .parseAs() + .parseAs() } } catch (e: Exception) { null @@ -140,22 +133,20 @@ class MangaUpdatesApi( } authClient.newCall( PUT( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/series/${track.remote_id}/rating", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() } else { authClient.newCall( - DELETE( - url = "$baseUrl/v1/series/${track.remote_id}/rating", - ), + DELETE(url = "$BASE_URL/v1/series/${track.remote_id}/rating"), ) .awaitSuccess() } } - suspend fun search(query: String): List { + suspend fun search(query: String): List { val body = buildJsonObject { put("search", query) put( @@ -166,25 +157,22 @@ class MangaUpdatesApi( }, ) } + return with(json) { client.newCall( POST( - url = "$baseUrl/v1/series/search", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/series/search", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() - .parseAs() - .let { obj -> - obj["results"]?.jsonArray?.map { element -> - json.decodeFromJsonElement(element.jsonObject["record"]!!) - } - } - .orEmpty() + .parseAs() + .results + .map { it.record } } } - suspend fun authenticate(username: String, password: String): Context? { + suspend fun authenticate(username: String, password: String): MUContext? { val body = buildJsonObject { put("username", username) put("password", password) @@ -192,20 +180,18 @@ class MangaUpdatesApi( return with(json) { client.newCall( PUT( - url = "$baseUrl/v1/account/login", - body = body.toString().toRequestBody(contentType), + url = "$BASE_URL/v1/account/login", + body = body.toString().toRequestBody(CONTENT_TYPE), ), ) .awaitSuccess() - .parseAs() - .let { obj -> - try { - json.decodeFromJsonElement(obj["context"]!!) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - null - } - } + .parseAs() + .context } } + + companion object { + private const val BASE_URL = "https://api.mangaupdates.com" + private val CONTENT_TYPE = "application/vnd.api+json".toMediaType() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt similarity index 91% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt index 77019cacd2..688de07007 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUContext.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Context( +data class MUContext( @SerialName("session_token") val sessionToken: String, val uid: Long, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt similarity index 78% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt index bed1f2657b..0ef38ca242 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUImage.kt @@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Image( - val url: Url? = null, +data class MUImage( + val url: MUUrl? = null, val height: Int? = null, val width: Int? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt similarity index 79% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt index edee5eedbc..11ad5d69d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUListItem.kt @@ -6,15 +6,15 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ListItem( - val series: Series? = null, +data class MUListItem( + val series: MUSeries? = null, @SerialName("list_id") val listId: Long? = null, - val status: Status? = null, + val status: MUStatus? = null, val priority: Int? = null, ) -fun ListItem.copyTo(track: MangaTrack): MangaTrack { +fun MUListItem.copyTo(track: MangaTrack): MangaTrack { return track.apply { this.status = listId ?: READING_LIST this.last_chapter_read = this@copyTo.status?.chapter?.toDouble() ?: 0.0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt new file mode 100644 index 0000000000..6b2a60cc77 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MULoginResponse.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.mangaupdates.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MULoginResponse( + val context: MUContext, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt similarity index 79% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt index 81daad2c9c..6f09de629f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURating.kt @@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import kotlinx.serialization.Serializable @Serializable -data class Rating( +data class MURating( val rating: Double? = null, ) -fun Rating.copyTo(track: MangaTrack): MangaTrack { +fun MURating.copyTo(track: MangaTrack): MangaTrack { return track.apply { this.score = rating ?: 0.0 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt similarity index 91% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt index bce28f6008..e01cc63df2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MURecord.kt @@ -6,13 +6,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Record( +data class MURecord( @SerialName("series_id") val seriesId: Long? = null, val title: String? = null, val url: String? = null, val description: String? = null, - val image: Image? = null, + val image: MUImage? = null, val type: String? = null, val year: String? = null, @SerialName("bayesian_rating") @@ -23,7 +23,7 @@ data class Record( val latestChapter: Int? = null, ) -fun Record.toTrackSearch(id: Long): MangaTrackSearch { +fun MURecord.toTrackSearch(id: Long): MangaTrackSearch { return MangaTrackSearch.create(id).apply { remote_id = this@toTrackSearch.seriesId ?: 0L title = this@toTrackSearch.title?.htmlDecode() ?: "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt new file mode 100644 index 0000000000..3e1771b0b6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSearch.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.mangaupdates.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MUSearchResult( + val results: List, +) + +@Serializable +data class MUSearchResultItem( + val record: MURecord, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt similarity index 89% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt index 261c857372..fa8e3feacd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUSeries.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Series( +data class MUSeries( val id: Long? = null, val title: String? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt similarity index 89% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt index 7320ac2e3d..99bf5a7af0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUStatus.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Status( +data class MUStatus( val volume: Int? = null, val chapter: Int? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt index f295d3bdc7..3e969e3b35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/MUUrl.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto import kotlinx.serialization.Serializable @Serializable -data class Url( +data class MUUrl( val original: String? = null, val thumb: String? = null, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/AnimeTrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/AnimeTrackSearch.kt index f9d30c79a5..b8e2565d35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/AnimeTrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/AnimeTrackSearch.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.track.model import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/MangaTrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/MangaTrackSearch.kt index 41c19cc978..4ec37d025a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/MangaTrackSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/MangaTrackSearch.kt @@ -1,3 +1,5 @@ +@file:Suppress("PropertyName") + package eu.kanade.tachiyomi.data.track.model import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 26d9fa9f89..9473ed8a52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.DeletableMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString @@ -254,7 +255,7 @@ class MyAnimeList(id: Long) : val oauth = api.getAccessToken(authCode) interceptor.setAuth(oauth) val username = api.getCurrentUser() - saveCredentials(username, oauth.access_token) + saveCredentials(username, oauth.accessToken) } catch (e: Throwable) { logout() } @@ -274,13 +275,13 @@ class MyAnimeList(id: Long) : trackPreferences.trackAuthExpired(this).set(true) } - fun saveOAuth(oAuth: OAuth?) { + fun saveOAuth(oAuth: MALOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } - fun loadOAuth(): OAuth? { + fun loadOAuth(): MALOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { 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 e2b8ac3603..de618c9fff 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 @@ -6,6 +6,16 @@ import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALAnime +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListAnimeItem +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListAnimeItemStatus +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListMangaItem +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALListMangaItemStatus +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -15,16 +25,6 @@ import eu.kanade.tachiyomi.util.PkceUtil import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.boolean -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.double -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import okhttp3.FormBody import okhttp3.Headers import okhttp3.OkHttpClient @@ -47,7 +47,7 @@ class MyAnimeListApi( private val authClient = client.newBuilder().addInterceptor(interceptor).build() - suspend fun getAccessToken(authCode: String): OAuth { + suspend fun getAccessToken(authCode: String): MALOAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() .add("client_id", CLIENT_ID) @@ -72,8 +72,8 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() - .let { it["name"]!!.jsonPrimitive.content } + .parseAs() + .name } } } @@ -88,17 +88,11 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray - .map { data -> data.jsonObject["node"]!!.jsonObject } - .map { node -> - val id = node["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } - .awaitAll() - .filter { trackSearch -> !trackSearch.publishing_type.contains("novel") } - } + .parseAs() + .data + .map { async { getMangaDetails(it.node.id) } } + .awaitAll() + .filter { !it.publishing_type.contains("novel") } } } } @@ -114,16 +108,10 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { - it["data"]!!.jsonArray - .map { data -> data.jsonObject["node"]!!.jsonObject } - .map { node -> - val id = node["id"]!!.jsonPrimitive.int - async { getAnimeDetails(id) } - } - .awaitAll() - } + .parseAs() + .data + .map { async { getAnimeDetails(it.node.id) } } + .awaitAll() } } } @@ -140,29 +128,19 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val obj = it.jsonObject MangaTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["title"]!!.jsonPrimitive.content - summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" - total_chapters = obj["num_chapters"]!!.jsonPrimitive.long - score = obj["mean"]?.jsonPrimitive?.doubleOrNull ?: -1.0 - cover_url = - obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content - ?: "" + remote_id = it.id + title = it.title + summary = it.synopsis + total_chapters = it.numChapters + score = it.mean + cover_url = it.covers.large tracking_url = "https://myanimelist.net/manga/$remote_id" - publishing_status = - obj["status"]!!.jsonPrimitive.content.replace("_", " ") - publishing_type = - obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(obj["start_date"]!!) - } catch (e: Exception) { - "" - } + publishing_status = it.status.replace("_", " ") + publishing_type = it.mediaType.replace("_", " ") + start_date = it.startDate ?: "" } } } @@ -181,29 +159,19 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() + .parseAs() .let { - val obj = it.jsonObject AnimeTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["title"]!!.jsonPrimitive.content - summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" - total_episodes = obj["num_episodes"]!!.jsonPrimitive.long - score = obj["mean"]?.jsonPrimitive?.doubleOrNull ?: -1.0 - cover_url = - obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content - ?: "" + remote_id = it.id + title = it.title + summary = it.synopsis + total_episodes = it.numEpisodes + score = it.mean + cover_url = it.covers.large tracking_url = "https://myanimelist.net/anime/$remote_id" - publishing_status = - obj["status"]!!.jsonPrimitive.content.replace("_", " ") - publishing_type = - obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(obj["start_date"]!!) - } catch (e: Exception) { - "" - } + publishing_status = it.status.replace("_", " ") + publishing_type = it.mediaType.replace("_", " ") + start_date = it.startDate ?: "" } } } @@ -231,7 +199,7 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() + .parseAs() .let { parseMangaItem(it, track) } } } @@ -258,7 +226,7 @@ class MyAnimeListApi( with(json) { authClient.newCall(request) .awaitSuccess() - .parseAs() + .parseAs() .let { parseAnimeItem(it, track) } } } @@ -289,12 +257,10 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(uri.toString())) .awaitSuccess() - .parseAs() - .let { obj -> - track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.long - obj.jsonObject["my_list_status"]?.jsonObject?.let { - parseMangaItem(it, track) - } + .parseAs() + .let { item -> + track.total_chapters = item.numChapters + item.myListStatus?.let { parseMangaItem(it, track) } } } } @@ -309,12 +275,10 @@ class MyAnimeListApi( with(json) { authClient.newCall(GET(uri.toString())) .awaitSuccess() - .parseAs() - .let { obj -> - track.total_episodes = obj["num_episodes"]!!.jsonPrimitive.long - obj.jsonObject["my_list_status"]?.jsonObject?.let { - parseAnimeItem(it, track) - } + .parseAs() + .let { item -> + track.total_episodes = item.numEpisodes + item.myListStatus?.let { parseAnimeItem(it, track) } } } } @@ -322,24 +286,15 @@ class MyAnimeListApi( suspend fun findListItems(query: String, offset: Int = 0): List { return withIOContext { - val json = getListPage(offset) - val obj = json.jsonObject - - val matches = obj["data"]!!.jsonArray - .filter { - it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains( - query, - ignoreCase = true, - ) - } - .map { - val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int - async { getMangaDetails(id) } - } + val myListSearchResult = getListPage(offset) + + val matches = myListSearchResult.data + .filter { it.node.title.contains(query, ignoreCase = true) } + .map { async { getMangaDetails(it.node.id) } } .awaitAll() // Check next page if there's more - if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { + if (!myListSearchResult.paging.next.isNullOrBlank()) { matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT) } else { matches @@ -349,24 +304,15 @@ class MyAnimeListApi( suspend fun findListItemsAnime(query: String, offset: Int = 0): List { return withIOContext { - val json = getListPage(offset) - val obj = json.jsonObject - - val matches = obj["data"]!!.jsonArray - .filter { - it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains( - query, - ignoreCase = true, - ) - } - .map { - val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int - async { getAnimeDetails(id) } - } + val myListSearchResult = getListPage(offset) + + val matches = myListSearchResult.data + .filter { it.node.title.contains(query, ignoreCase = true) } + .map { async { getAnimeDetails(it.node.id) } } .awaitAll() // Check next page if there's more - if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { + if (!myListSearchResult.paging.next.isNullOrBlank()) { matches + findListItemsAnime(query, offset + LIST_PAGINATION_AMOUNT) } else { matches @@ -374,7 +320,7 @@ class MyAnimeListApi( } } - private suspend fun getListPage(offset: Int): JsonObject { + private suspend fun getListPage(offset: Int): MALUserSearchResult { return withIOContext { val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() .appendQueryParameter("fields", "list_status{start_date,finish_date}") @@ -395,47 +341,25 @@ class MyAnimeListApi( } } - private fun parseMangaItem(response: JsonObject, track: MangaTrack): MangaTrack { - val obj = response.jsonObject + private fun parseMangaItem(listStatus: MALListMangaItemStatus, track: MangaTrack): MangaTrack { return track.apply { - val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean - status = if (isRereading) { - MyAnimeList.REREADING - } else { - getStatus( - obj["status"]?.jsonPrimitive?.content, - ) - } - last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - obj["start_date"]?.let { - started_reading_date = parseDate(it.jsonPrimitive.content) - } - obj["finish_date"]?.let { - finished_reading_date = parseDate(it.jsonPrimitive.content) - } + val isRereading = listStatus.isRereading + status = if (isRereading) MyAnimeList.REREADING else getStatus(listStatus.status) + last_chapter_read = listStatus.numChaptersRead + score = listStatus.score.toDouble() + listStatus.startDate?.let { started_reading_date = parseDate(it) } + listStatus.finishDate?.let { finished_reading_date = parseDate(it) } } } - private fun parseAnimeItem(response: JsonObject, track: AnimeTrack): AnimeTrack { - val obj = response.jsonObject + private fun parseAnimeItem(listStatus: MALListAnimeItemStatus, track: AnimeTrack): AnimeTrack { return track.apply { - val isRereading = obj["is_rewatching"]!!.jsonPrimitive.boolean - status = if (isRereading) { - MyAnimeList.REWATCHING - } else { - getStatus( - obj["status"]!!.jsonPrimitive.content, - ) - } - last_episode_seen = obj["num_episodes_watched"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - obj["start_date"]?.let { - started_watching_date = parseDate(it.jsonPrimitive.content) - } - obj["finish_date"]?.let { - finished_watching_date = parseDate(it.jsonPrimitive.content) - } + val isRewatching = listStatus.isRewatching + status = if (isRewatching) MyAnimeList.REWATCHING else getStatus(listStatus.status) + last_episode_seen = listStatus.numEpisodesWatched + score = listStatus.score.toDouble() + listStatus.startDate?.let { started_watching_date = parseDate(it) } + listStatus.finishDate?.let { finished_watching_date = parseDate(it) } } } @@ -481,10 +405,10 @@ class MyAnimeListApi( .appendPath("my_list_status") .build() - fun refreshTokenRequest(oauth: OAuth): Request { + fun refreshTokenRequest(oauth: MALOAuth): Request { val formBody: RequestBody = FormBody.Builder() .add("client_id", CLIENT_ID) - .add("refresh_token", oauth.refresh_token) + .add("refresh_token", oauth.refreshToken) .add("grant_type", "refresh_token") .build() @@ -492,7 +416,7 @@ class MyAnimeListApi( // request is called by the interceptor itself so it doesn't reach // the part where the token is added automatically. val headers = Headers.Builder() - .add("Authorization", "Bearer ${oauth.access_token}") + .add("Authorization", "Bearer ${oauth.accessToken}") .build() return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers) 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 2aa6578080..34d9e4bfdf 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 @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json import okhttp3.Interceptor @@ -12,7 +13,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor private val json: Json by injectLazy() - private var oauth: OAuth? = myanimelist.loadOAuth() + private var oauth: MALOAuth? = myanimelist.loadOAuth() private val tokenExpired get() = myanimelist.getIfAuthExpired() override fun intercept(chain: Interceptor.Chain): Response { @@ -31,7 +32,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor // Add the authorization header to the original request val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Animetail v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() @@ -42,12 +43,12 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor * Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token * and the oauth object. */ - fun setAuth(oauth: OAuth?) { + fun setAuth(oauth: MALOAuth?) { this.oauth = oauth myanimelist.saveOAuth(oauth) } - private fun refreshToken(chain: Interceptor.Chain): OAuth = synchronized(this) { + private fun refreshToken(chain: Interceptor.Chain): MALOAuth = synchronized(this) { if (tokenExpired) throw MALTokenExpired() oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } @@ -64,7 +65,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor return runCatching { if (response.isSuccessful) { - with(json) { response.parseAs() } + with(json) { response.parseAs() } } else { response.close() null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt similarity index 74% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt index 8c29e8070e..bcb412707c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListUtils.kt @@ -2,20 +2,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack -import kotlinx.serialization.Serializable - -@Serializable -data class OAuth( - val token_type: String, - val refresh_token: String, - val access_token: String, - val expires_in: Long, - val created_at: Long = System.currentTimeMillis(), -) { - // Assumes expired a minute earlier - private val adjustedExpiresIn: Long = (expires_in - 60) * 1000 - fun isExpired() = created_at + adjustedExpiresIn < System.currentTimeMillis() -} fun MangaTrack.toMyAnimeListStatus() = when (status) { MyAnimeList.READING -> "reading" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt new file mode 100644 index 0000000000..db1d865e39 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALAnime.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALAnime( + val id: Long, + val title: String, + val synopsis: String = "", + @SerialName("num_episodes") + val numEpisodes: Long, + val mean: Double = -1.0, + @SerialName("main_picture") + val covers: MALAnimeCovers, + val status: String, + @SerialName("media_type") + val mediaType: String, + @SerialName("start_date") + val startDate: String?, +) + +@Serializable +data class MALAnimeCovers( + val large: String = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt new file mode 100644 index 0000000000..6ec5b8067a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALList.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALListAnimeItem( + @SerialName("num_episodes") + val numEpisodes: Long, + @SerialName("my_list_status") + val myListStatus: MALListAnimeItemStatus?, +) + +@Serializable +data class MALListAnimeItemStatus( + @SerialName("is_rewatching") + val isRewatching: Boolean, + val status: String, + @SerialName("num_episodes_watched") + val numEpisodesWatched: Double, + val score: Int, + @SerialName("start_date") + val startDate: String?, + @SerialName("finish_date") + val finishDate: String?, +) + +@Serializable +data class MALListMangaItem( + @SerialName("num_chapters") + val numChapters: Long, + @SerialName("my_list_status") + val myListStatus: MALListMangaItemStatus?, +) + +@Serializable +data class MALListMangaItemStatus( + @SerialName("is_rereading") + val isRereading: Boolean, + val status: String, + @SerialName("num_chapters_read") + val numChaptersRead: Double, + val score: Int, + @SerialName("start_date") + val startDate: String?, + @SerialName("finish_date") + val finishDate: String?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt new file mode 100644 index 0000000000..c4ab92ee92 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALManga.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALManga( + val id: Long, + val title: String, + val synopsis: String = "", + @SerialName("num_chapters") + val numChapters: Long, + val mean: Double = -1.0, + @SerialName("main_picture") + val covers: MALMangaCovers, + val status: String, + @SerialName("media_type") + val mediaType: String, + @SerialName("start_date") + val startDate: String?, +) + +@Serializable +data class MALMangaCovers( + val large: String = "", +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt new file mode 100644 index 0000000000..2f3a5f8e88 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALOAuth.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MALOAuth( + @SerialName("token_type") + val tokenType: String, + @SerialName("refresh_token") + val refreshToken: String, + @SerialName("access_token") + val accessToken: String, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("created_at") + val createdAt: Long = System.currentTimeMillis(), +) { + // Assumes expired a minute earlier + private val adjustedExpiresIn: Long = (expiresIn - 60) * 1000 + + fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt new file mode 100644 index 0000000000..51ef2a6a48 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALSearch.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALSearchResult( + val data: List, +) + +@Serializable +data class MALSearchResultNode( + val node: MALSearchResultItem, +) + +@Serializable +data class MALSearchResultItem( + val id: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt new file mode 100644 index 0000000000..a59974abd3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUser.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALUser( + val name: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt new file mode 100644 index 0000000000..fad099a24b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserListSearch.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.track.myanimelist.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MALUserSearchResult( + val data: List, + val paging: MALUserSearchPaging, +) + +@Serializable +data class MALUserSearchItem( + val node: MALUserSearchItemNode, +) + +@Serializable +data class MALUserSearchPaging( + val next: String?, +) + +@Serializable +data class MALUserSearchItemNode( + val id: Int, + val title: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 15dd3fb20a..f2db8e0134 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.DeletableMangaTracker import eu.kanade.tachiyomi.data.track.MangaTracker import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString @@ -161,7 +162,7 @@ class Shikimori(id: Long) : track.library_id = remoteTrack.library_id track.copyPersonalFrom(remoteTrack) track.total_chapters = remoteTrack.total_chapters - } + } ?: throw Exception("Could not find manga") return track } @@ -170,7 +171,7 @@ class Shikimori(id: Long) : track.library_id = remoteTrack.library_id track.copyPersonalFrom(remoteTrack) track.total_episodes = remoteTrack.total_episodes - } + } ?: throw Exception("Could not find anime") return track } @@ -223,19 +224,19 @@ class Shikimori(id: Long) : val oauth = api.accessToken(code) interceptor.newAuth(oauth) val user = api.getCurrentUser() - saveCredentials(user.toString(), oauth.access_token) + saveCredentials(user.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: SMOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): SMOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 524eed0fb6..d9fa66a82f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -6,6 +6,11 @@ import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddEntryResponse +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMEntry +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListEntry import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -13,15 +18,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.double -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject import okhttp3.FormBody @@ -57,14 +54,13 @@ class ShikimoriApi( } authClient.newCall( POST( - "$apiUrl/v2/user_rates", + "$API_URL/v2/user_rates", body = payload.toString().toRequestBody(jsonMime), ), ).awaitSuccess() - .parseAs() + .parseAs() .let { - track.library_id = - it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request + track.library_id = it.id } track } @@ -79,7 +75,7 @@ class ShikimoriApi( suspend fun deleteLibManga(track: DomainMangaTrack) { withIOContext { authClient - .newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}")) + .newCall(DELETE("$API_URL/v2/user_rates/${track.libraryId}")) .awaitSuccess() } } @@ -99,14 +95,13 @@ class ShikimoriApi( } authClient.newCall( POST( - "$apiUrl/v2/user_rates", + "$API_URL/v2/user_rates", body = payload.toString().toRequestBody(jsonMime), ), ).awaitSuccess() - .parseAs() + .parseAs() .let { - track.library_id = - it["id"]!!.jsonPrimitive.long // save id of the entry for possible future delete request + track.library_id = it.id } track } @@ -121,14 +116,14 @@ class ShikimoriApi( suspend fun deleteLibAnime(track: DomainAnimeTrack) { withIOContext { authClient - .newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}")) + .newCall(DELETE("$API_URL/v2/user_rates/${track.libraryId}")) .awaitSuccess() } } suspend fun search(search: String): List { return withIOContext { - val url = "$apiUrl/mangas".toUri().buildUpon() + val url = "$API_URL/mangas".toUri().buildUpon() .appendQueryParameter("order", "popularity") .appendQueryParameter("search", search) .appendQueryParameter("limit", "20") @@ -136,19 +131,15 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - response.map { - jsonToSearch(it.jsonObject) - } - } + .parseAs>() + .map { it.toMangaTrack(trackId) } } } } suspend fun searchAnime(search: String): List { return withIOContext { - val url = "$apiUrl/animes".toUri().buildUpon() + val url = "$API_URL/animes".toUri().buildUpon() .appendQueryParameter("order", "popularity") .appendQueryParameter("search", search) .appendQueryParameter("limit", "20") @@ -156,84 +147,24 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - response.map { - jsonToAnimeSearch(it.jsonObject) - } - } + .parseAs>() + .map { it.toAnimeTrack(trackId) } } } } - private fun jsonToSearch(obj: JsonObject): MangaTrackSearch { - return MangaTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name"]!!.jsonPrimitive.content - total_chapters = obj["chapters"]!!.jsonPrimitive.long - cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content - summary = "" - score = obj["score"]!!.jsonPrimitive.double - tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content - publishing_status = obj["status"]!!.jsonPrimitive.content - publishing_type = obj["kind"]!!.jsonPrimitive.content - start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: "" - } - } - - private fun jsonToAnimeSearch(obj: JsonObject): AnimeTrackSearch { - return AnimeTrackSearch.create(trackId).apply { - remote_id = obj["id"]!!.jsonPrimitive.long - title = obj["name"]!!.jsonPrimitive.content - total_episodes = obj["episodes"]!!.jsonPrimitive.long - cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content - summary = "" - score = obj["score"]!!.jsonPrimitive.double - tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content - publishing_status = obj["status"]!!.jsonPrimitive.content - publishing_type = obj["kind"]!!.jsonPrimitive.content - start_date = obj.get("aired_on")!!.jsonPrimitive.contentOrNull ?: "" - } - } - - private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): MangaTrack { - return MangaTrack.create(trackId).apply { - title = mangas["name"]!!.jsonPrimitive.content - remote_id = obj["id"]!!.jsonPrimitive.long - total_chapters = mangas["chapters"]!!.jsonPrimitive.long - library_id = obj["id"]!!.jsonPrimitive.long - last_chapter_read = obj["chapters"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) - tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content - } - } - - private fun jsonToAnimeTrack(obj: JsonObject, animes: JsonObject): AnimeTrack { - return AnimeTrack.create(trackId).apply { - title = animes["name"]!!.jsonPrimitive.content - remote_id = obj["id"]!!.jsonPrimitive.long - total_episodes = animes["episodes"]!!.jsonPrimitive.long - library_id = obj["id"]!!.jsonPrimitive.long - last_episode_seen = obj["episodes"]!!.jsonPrimitive.double - score = obj["score"]!!.jsonPrimitive.int.toDouble() - status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) - tracking_url = baseUrl + animes["url"]!!.jsonPrimitive.content - } - } - suspend fun findLibManga(track: MangaTrack, userId: String): MangaTrack? { return withIOContext { - val urlMangas = "$apiUrl/mangas".toUri().buildUpon() + val urlMangas = "$API_URL/mangas".toUri().buildUpon() .appendPath(track.remote_id.toString()) .build() - val mangas = with(json) { + val manga = with(json) { authClient.newCall(GET(urlMangas.toString())) .awaitSuccess() - .parseAs() + .parseAs() } - val url = "$apiUrl/v2/user_rates".toUri().buildUpon() + val url = "$API_URL/v2/user_rates".toUri().buildUpon() .appendQueryParameter("user_id", userId) .appendQueryParameter("target_id", track.remote_id.toString()) .appendQueryParameter("target_type", "Manga") @@ -241,15 +172,14 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - if (response.size > 1) { - throw Exception("Too much mangas in response") - } - val entry = response.map { - jsonToTrack(it.jsonObject, mangas) + .parseAs>() + .let { entries -> + if (entries.size > 1) { + throw Exception("Too many manga in response") } - entry.firstOrNull() + entries + .map { it.toMangaTrack(trackId, manga) } + .firstOrNull() } } } @@ -257,16 +187,16 @@ class ShikimoriApi( suspend fun findLibAnime(track: AnimeTrack, user_id: String): AnimeTrack? { return withIOContext { - val urlAnimes = "$apiUrl/animes".toUri().buildUpon() + val urlAnimes = "$API_URL/animes".toUri().buildUpon() .appendPath(track.remote_id.toString()) .build() - val animes = with(json) { + val anime = with(json) { authClient.newCall(GET(urlAnimes.toString())) .awaitSuccess() - .parseAs() + .parseAs() } - val url = "$apiUrl/v2/user_rates".toUri().buildUpon() + val url = "$API_URL/v2/user_rates".toUri().buildUpon() .appendQueryParameter("user_id", user_id) .appendQueryParameter("target_id", track.remote_id.toString()) .appendQueryParameter("target_type", "Anime") @@ -274,15 +204,14 @@ class ShikimoriApi( with(json) { authClient.newCall(GET(url.toString())) .awaitSuccess() - .parseAs() - .let { response -> - if (response.size > 1) { - throw Exception("Too much animes in response") + .parseAs>() + .let { entries -> + if (entries.size > 1) { + throw Exception("Too many manga in response") } - val entry = response.map { - jsonToAnimeTrack(it.jsonObject, animes) - } - entry.firstOrNull() + entries + .map { it.toAnimeTrack(trackId, anime) } + .firstOrNull() } } } @@ -290,16 +219,14 @@ class ShikimoriApi( suspend fun getCurrentUser(): Int { return with(json) { - authClient.newCall(GET("$apiUrl/users/whoami")) + authClient.newCall(GET("$API_URL/users/whoami")) .awaitSuccess() - .parseAs() - .let { - it["id"]!!.jsonPrimitive.int - } + .parseAs() + .id } } - suspend fun accessToken(code: String): OAuth { + suspend fun accessToken(code: String): SMOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) @@ -310,39 +237,39 @@ class ShikimoriApi( } private fun accessTokenRequest(code: String) = POST( - oauthUrl, + OAUTH_URL, body = FormBody.Builder() .add("grant_type", "authorization_code") - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .add("code", code) - .add("redirect_uri", redirectUrl) + .add("redirect_uri", REDIRECT_URL) .build(), ) companion object { - private const val clientId = "aOAYRqOLwxpA8skpcQIXetNy4cw2rn2fRzScawlcQ5U" - private const val clientSecret = "jqjmORn6bh2046ulkm4lHEwJ3OA1RmO3FD2sR9f6Clw" + const val BASE_URL = "https://shikimori.one" + private const val API_URL = "$BASE_URL/api" + private const val OAUTH_URL = "$BASE_URL/oauth/token" + private const val LOGIN_URL = "$BASE_URL/oauth/authorize" - private const val baseUrl = "https://shikimori.one" - private const val apiUrl = "$baseUrl/api" - private const val oauthUrl = "$baseUrl/oauth/token" - private const val loginUrl = "$baseUrl/oauth/authorize" + private const val REDIRECT_URL = "animetail://shikimori-auth" - private const val redirectUrl = "animetail://shikimori-auth" + private const val CLIENT_ID = "aOAYRqOLwxpA8skpcQIXetNy4cw2rn2fRzScawlcQ5U" + private const val CLIENT_SECRET = "jqjmORn6bh2046ulkm4lHEwJ3OA1RmO3FD2sR9f6Clw" - fun authUrl(): Uri = loginUrl.toUri().buildUpon() - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", redirectUrl) + fun authUrl(): Uri = LOGIN_URL.toUri().buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) + .appendQueryParameter("redirect_uri", REDIRECT_URL) .appendQueryParameter("response_type", "code") .build() fun refreshTokenRequest(token: String) = POST( - oauthUrl, + OAUTH_URL, body = FormBody.Builder() .add("grant_type", "refresh_token") - .add("client_id", clientId) - .add("client_secret", clientSecret) + .add("client_id", CLIENT_ID) + .add("client_secret", CLIENT_SECRET) .add("refresh_token", token) .build(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt index 667c431453..4f2123fc0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.track.shikimori import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth +import eu.kanade.tachiyomi.data.track.shikimori.dto.isExpired import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response @@ -13,34 +15,34 @@ class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = shikimori.restoreToken() + private var oauth: SMOAuth? = shikimori.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori") - val refreshToken = currAuth.refresh_token!! + val refreshToken = currAuth.refreshToken!! // Refresh access token if expired. if (currAuth.isExpired()) { val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken)) if (response.isSuccessful) { - newAuth(json.decodeFromString(response.body.string())) + newAuth(json.decodeFromString(response.body.string())) } else { response.close() } } // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .addHeader("Authorization", "Bearer ${oauth!!.accessToken}") .header("User-Agent", "Animetail v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: SMOAuth?) { this.oauth = oauth shikimori.saveToken(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt similarity index 78% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt index bdcc6e2693..5e2225d1ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt @@ -2,19 +2,6 @@ package eu.kanade.tachiyomi.data.track.shikimori import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack -import kotlinx.serialization.Serializable - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val created_at: Long, - val expires_in: Long, - val refresh_token: String?, -) - -// Access token lives 1 day -fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600) fun MangaTrack.toShikimoriStatus() = when (status) { Shikimori.READING -> "watching" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddEntryResponse.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddEntryResponse.kt new file mode 100644 index 0000000000..a3f65bd1f4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMAddEntryResponse.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SMAddEntryResponse( + val id: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMEntry.kt new file mode 100644 index 0000000000..3d8d159eee --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMEntry.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch +import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SMEntry( + val id: Long, + val name: String, + val chapters: Long?, + val episodes: Long?, + val image: SUEntryCover, + val score: Double, + val url: String, + val status: String, + val kind: String, + @SerialName("aired_on") + val airedOn: String?, +) { + fun toMangaTrack(trackId: Long): MangaTrackSearch { + return MangaTrackSearch.create(trackId).apply { + remote_id = this@SMEntry.id + title = name + total_chapters = chapters!! + cover_url = ShikimoriApi.BASE_URL + image.preview + summary = "" + score = this@SMEntry.score + tracking_url = ShikimoriApi.BASE_URL + url + publishing_status = this@SMEntry.status + publishing_type = kind + start_date = airedOn ?: "" + } + } + + fun toAnimeTrack(trackId: Long): AnimeTrackSearch { + return AnimeTrackSearch.create(trackId).apply { + remote_id = this@SMEntry.id + title = name + total_episodes = episodes!! + cover_url = ShikimoriApi.BASE_URL + image.preview + summary = "" + score = this@SMEntry.score + tracking_url = ShikimoriApi.BASE_URL + url + publishing_status = this@SMEntry.status + publishing_type = kind + start_date = airedOn ?: "" + } + } +} + +@Serializable +data class SUEntryCover( + val preview: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt new file mode 100644 index 0000000000..e041048010 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMOAuth.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SMOAuth( + @SerialName("access_token") + val accessToken: String, + @SerialName("token_type") + val tokenType: String, + @SerialName("created_at") + val createdAt: Long, + @SerialName("expires_in") + val expiresIn: Long, + @SerialName("refresh_token") + val refreshToken: String?, +) + +// Access token lives 1 day +fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt new file mode 100644 index 0000000000..1b9ed6cdb1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SMUser( + val id: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt new file mode 100644 index 0000000000..a77a85113e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.data.track.shikimori.dto + +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack +import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi +import eu.kanade.tachiyomi.data.track.shikimori.toTrackStatus +import kotlinx.serialization.Serializable + +@Serializable +data class SMUserListEntry( + val id: Long, + val chapters: Double, + val episodes: Double, + val score: Int, + val status: String, +) { + fun toMangaTrack(trackId: Long, manga: SMEntry): MangaTrack { + return MangaTrack.create(trackId).apply { + title = manga.name + remote_id = this@SMUserListEntry.id + total_chapters = manga.chapters!! + library_id = this@SMUserListEntry.id + last_chapter_read = this@SMUserListEntry.chapters + score = this@SMUserListEntry.score.toDouble() + status = toTrackStatus(this@SMUserListEntry.status) + tracking_url = ShikimoriApi.BASE_URL + manga.url + } + } + + fun toAnimeTrack(trackId: Long, anime: SMEntry): AnimeTrack { + return AnimeTrack.create(trackId).apply { + title = anime.name + remote_id = this@SMUserListEntry.id + total_episodes = anime.chapters!! + library_id = this@SMUserListEntry.id + last_episode_seen = this@SMUserListEntry.episodes + score = this@SMUserListEntry.score.toDouble() + status = toTrackStatus(this@SMUserListEntry.status) + tracking_url = ShikimoriApi.BASE_URL + anime.url + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/Simkl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/Simkl.kt index 440c212848..19b3b41d8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/Simkl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/Simkl.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.BaseTracker import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklOAuth import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString @@ -122,19 +123,19 @@ class Simkl(id: Long) : BaseTracker(id, "Simkl"), AnimeTracker { val oauth = api.accessToken(code) interceptor.newAuth(oauth) val user = api.getCurrentUser() - saveCredentials(user.toString(), oauth.access_token) + saveCredentials(user.toString(), oauth.accessToken) } catch (e: Throwable) { logout() } } - fun saveToken(oauth: OAuth?) { + fun saveToken(oauth: SimklOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oauth)) } - fun restoreToken(): OAuth? { + fun restoreToken(): SimklOAuth? { return try { - json.decodeFromString(trackPreferences.trackToken(this).get()) + json.decodeFromString(trackPreferences.trackToken(this).get()) } catch (e: Exception) { null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklApi.kt index 05e9f08e68..059e7e42e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklApi.kt @@ -1,10 +1,15 @@ package eu.kanade.tachiyomi.data.track.simkl import android.net.Uri +import android.util.Log import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack -import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklOAuth +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklSearchResult +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklSyncResult +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklSyncWatched +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklUser import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.awaitSuccess @@ -12,21 +17,9 @@ import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.addJsonObject -import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.double -import kotlinx.serialization.json.int -import kotlinx.serialization.json.intOrNull -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject @@ -65,7 +58,7 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) } }.toString().toRequestBody(jsonMime) authClient.newCall( - POST("$apiUrl/sync/add-to-list", body = payload), + POST("$API_URL/sync/add-to-list", body = payload), ).awaitSuccess() } @@ -83,11 +76,11 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) if (track.score == 0.0) { authClient.newCall( - POST("$apiUrl/sync/ratings/remove", body = payload), + POST("$API_URL/sync/ratings/remove", body = payload), ).awaitSuccess() } else { authClient.newCall( - POST("$apiUrl/sync/ratings", body = payload), + POST("$API_URL/sync/ratings", body = payload), ).awaitSuccess() } } @@ -95,11 +88,11 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) private suspend fun updateProgress(track: AnimeTrack) { // first remove authClient.newCall( - POST("$apiUrl/sync/history/remove", body = buildProgressObject(track, false)), + POST("$API_URL/sync/history/remove", body = buildProgressObject(track, false)), ).awaitSuccess() // then add again authClient.newCall( - POST("$apiUrl/sync/history", body = buildProgressObject(track, true)), + POST("$API_URL/sync/history", body = buildProgressObject(track, true)), ).awaitSuccess() } @@ -149,64 +142,17 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) suspend fun searchAnime(search: String, type: String): List { return withIOContext { - val searchUrl = "$apiUrl/search/$type".toUri().buildUpon() + val searchUrl = "$API_URL/search/$type".toUri().buildUpon() .appendQueryParameter("q", search) .appendQueryParameter("extended", "full") - .appendQueryParameter("client_id", clientId) + .appendQueryParameter("client_id", CLIENT_ID) .build() with(json) { client.newCall(GET(searchUrl.toString())) .awaitSuccess() - .parseAs() - .let { response -> - response.map { - jsonToAnimeSearch(it.jsonObject, type) - } - } - } - } - } - - private fun jsonToAnimeSearch(obj: JsonObject, type: String): AnimeTrackSearch { - return AnimeTrackSearch.create(TrackerManager.SIMKL).apply { - remote_id = obj["ids"]!!.jsonObject["simkl_id"]!!.jsonPrimitive.long - title = obj["title_romaji"]?.jsonPrimitive?.content ?: obj["title"]!!.jsonPrimitive.content - total_episodes = obj["ep_count"]?.jsonPrimitive?.longOrNull ?: 1 - cover_url = "https://simkl.in/posters/" + obj["poster"]!!.jsonPrimitive.content + "_m.webp" - summary = obj["all_titles"]?.jsonArray - ?.joinToString("\n", "All titles:\n") { it.jsonPrimitive.content } ?: "" - - tracking_url = obj["url"]!!.jsonPrimitive.content - publishing_status = obj["status"]?.jsonPrimitive?.content ?: "ended" - publishing_type = obj["type"]?.jsonPrimitive?.content ?: type - start_date = obj["year"]?.jsonPrimitive?.intOrNull?.toString() ?: "" - } - } - - private fun jsonToAnimeTrack( - obj: JsonObject, - typeName: String, - type: String, - statusString: String, - ): AnimeTrack { - return AnimeTrack.create(TrackerManager.SIMKL).apply { - title = obj[typeName]!!.jsonObject["title"]!!.jsonPrimitive.content - val id = obj[typeName]!!.jsonObject["ids"]!!.jsonObject["simkl"]!!.jsonPrimitive.long - remote_id = id - if (typeName != "movie") { - total_episodes = - obj["total_episodes_count"]!! - .jsonPrimitive.long - last_episode_seen = - obj["watched_episodes_count"]!! - .jsonPrimitive.double - } else { - total_episodes = 1 - last_episode_seen = if (statusString == "completed") 1.0 else 0.0 + .parseAs>() + .map { it.toTrackSearch(type) } } - score = obj["user_rating"]!!.jsonPrimitive.intOrNull?.toDouble() ?: 0.0 - status = toTrackStatus(statusString) - tracking_url = "/$type/$id" } } @@ -221,58 +167,55 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) put("simkl", track.remote_id) } }.toString().toRequestBody(jsonMime) - val foundAnime = - with(json) { - authClient.newCall( - POST("$apiUrl/sync/watched", body = payload), - ) - .awaitSuccess() - .parseAs() - .firstOrNull()?.jsonObject ?: return@withIOContext null - } + val foundAnime = with(json) { + authClient.newCall( + POST("$API_URL/sync/watched", body = payload), + ) + .awaitSuccess() + .parseAs>() + .firstOrNull() ?: return@withIOContext null + } - if (foundAnime["result"]?.jsonPrimitive?.booleanOrNull != true) return@withIOContext null - val lastWatched = foundAnime["last_watched"]?.jsonPrimitive?.contentOrNull ?: return@withIOContext null - val status = foundAnime["list"]!!.jsonPrimitive.content + Log.i("SOMETHING-IDK", foundAnime.toString()) + + if (foundAnime.result != true) return@withIOContext null + val lastWatched = foundAnime.lastWatched ?: return@withIOContext null + val status = foundAnime.list ?: return@withIOContext null val type = track.tracking_url .substringAfter("/") .substringBefore("/") val queryType = if (type == "tv") "shows" else type - val url = "$apiUrl/sync/all-items/$queryType/$status".toUri().buildUpon() + val url = "$API_URL/sync/all-items/$queryType/$status".toUri().buildUpon() .appendQueryParameter("date_from", lastWatched) .build() val typeName = if (type == "movies") "movie" else "show" - val listAnime = - with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs()[queryType]!!.jsonArray - .firstOrNull { - it.jsonObject[typeName] - ?.jsonObject?.get("ids") - ?.jsonObject?.get("simkl") - ?.jsonPrimitive?.long == track.remote_id - }?.jsonObject ?: return@withIOContext null - } - jsonToAnimeTrack(listAnime, typeName, type, status) + val listAnime = with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .getFromType(queryType) + ?.firstOrNull { item -> + item.getFromType(typeName).ids.simkl == track.remote_id + } ?: return@withIOContext null + } + + listAnime.toAnimeTrack(typeName, type, status) } } fun getCurrentUser(): Int { return runBlocking { with(json) { - authClient.newCall(GET("$apiUrl/users/settings")) + authClient.newCall(GET("$API_URL/users/settings")) .awaitSuccess() - .parseAs() - .let { - it["account"]!!.jsonObject["id"]!!.jsonPrimitive.int - } + .parseAs() + .account.id } } } - suspend fun accessToken(code: String): OAuth { + suspend fun accessToken(code: String): SimklOAuth { return withIOContext { with(json) { client.newCall(accessTokenRequest(code)) @@ -283,32 +226,33 @@ class SimklApi(private val client: OkHttpClient, interceptor: SimklInterceptor) } private fun accessTokenRequest(code: String) = POST( - oauthUrl, + OAUTH_URL, body = buildJsonObject { put("code", code) - put("client_id", clientId) - put("client_secret", clientSecret) - put("redirect_uri", redirectUrl) + put("client_id", CLIENT_ID) + put("client_secret", CLIENT_SECRET) + put("redirect_uri", REDIRECT_URL) put("grant_type", "authorization_code") }.toString().toRequestBody(jsonMime), ) companion object { - const val clientId = "15b6dd71be6df203b390a6cd7d9633d000f1828536380f0ba9df2dda69348a9a" - private const val clientSecret = "f4aaa510f00ce5e08109d6e37015606be0199c439ce3a25761562f6efbd0921c" + const val CLIENT_ID = "15b6dd71be6df203b390a6cd7d9633d000f1828536380f0ba9df2dda69348a9a" + private const val CLIENT_SECRET = "f4aaa510f00ce5e08109d6e37015606be0199c439ce3a25761562f6efbd0921c" - private const val baseUrl = "https://simkl.com" - private const val apiUrl = "https://api.simkl.com" - private const val oauthUrl = "$apiUrl/oauth/token" - private const val loginUrl = "$baseUrl/oauth/authorize" + private const val BASE_URL = "https://simkl.com" + private const val API_URL = "https://api.simkl.com" + private const val OAUTH_URL = "$API_URL/oauth/token" + private const val LOGIN_URL = "$BASE_URL/oauth/authorize" + const val POSTERS_URL = "https://simkl.in/posters/" - private const val redirectUrl = "animetail://simkl-auth" + private const val REDIRECT_URL = "animetail://simkl-auth" fun authUrl(): Uri = - loginUrl.toUri().buildUpon() + LOGIN_URL.toUri().buildUpon() .appendQueryParameter("response_type", "code") - .appendQueryParameter("client_id", clientId) - .appendQueryParameter("redirect_uri", redirectUrl) + .appendQueryParameter("client_id", CLIENT_ID) + .appendQueryParameter("redirect_uri", REDIRECT_URL) .build() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklInterceptor.kt index 3cdea1b337..912cca7da3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklInterceptor.kt @@ -1,7 +1,8 @@ package eu.kanade.tachiyomi.data.track.simkl import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.data.track.simkl.SimklApi.Companion.clientId +import eu.kanade.tachiyomi.data.track.simkl.SimklApi.Companion.CLIENT_ID +import eu.kanade.tachiyomi.data.track.simkl.dto.SimklOAuth import okhttp3.Interceptor import okhttp3.Response @@ -10,7 +11,7 @@ class SimklInterceptor(val simkl: Simkl) : Interceptor { /** * OAuth object used for authenticated requests. */ - private var oauth: OAuth? = simkl.restoreToken() + private var oauth: SimklOAuth? = simkl.restoreToken() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -19,15 +20,15 @@ class SimklInterceptor(val simkl: Simkl) : Interceptor { // Add the authorization header to the original request. val authRequest = originalRequest.newBuilder() - .addHeader("Authorization", "Bearer ${oauth.access_token}") - .addHeader("simkl-api-key", clientId) + .addHeader("Authorization", "Bearer ${oauth.accessToken}") + .addHeader("simkl-api-key", CLIENT_ID) .header("User-Agent", "Animetail v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() return chain.proceed(authRequest) } - fun newAuth(oauth: OAuth?) { + fun newAuth(oauth: SimklOAuth?) { this.oauth = oauth simkl.saveToken(oauth) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklUtils.kt similarity index 82% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklModels.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklUtils.kt index 838ee18b1d..3d2c4f436c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/SimklUtils.kt @@ -1,14 +1,6 @@ package eu.kanade.tachiyomi.data.track.simkl import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack -import kotlinx.serialization.Serializable - -@Serializable -data class OAuth( - val access_token: String, - val token_type: String, - val scope: String, -) fun AnimeTrack.toSimklStatus() = when (status) { Simkl.WATCHING -> "watching" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklOAuth.kt new file mode 100644 index 0000000000..ecc51149f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklOAuth.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.data.track.simkl.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SimklOAuth( + @SerialName("access_token") + val accessToken: String, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSearch.kt new file mode 100644 index 0000000000..a40b60c7f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSearch.kt @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.data.track.simkl.dto + +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch +import eu.kanade.tachiyomi.data.track.simkl.SimklApi.Companion.POSTERS_URL +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SimklSearchResult( + val ids: SimlkSearchResultIds, + @SerialName("title_romaji") + val titleRomaji: String?, + val title: String?, + @SerialName("ep_count") + val epCount: Long?, + val poster: String?, + @SerialName("all_titles") + val allTitles: List?, + val url: String, + val status: String?, + val type: String?, + val year: Int?, +) { + fun toTrackSearch(fallbackType: String): AnimeTrackSearch { + return AnimeTrackSearch.create(TrackerManager.SIMKL).apply { + remote_id = ids.simklId + title = titleRomaji ?: this@SimklSearchResult.title!! + total_episodes = epCount ?: 1 + cover_url = poster?.let { "$POSTERS_URL${it}_m.webp" } ?: "" + summary = allTitles?.joinToString("\n", prefix = "All titles:\n") ?: "" + tracking_url = url + publishing_status = this@SimklSearchResult.status ?: "ended" + publishing_type = type ?: fallbackType + start_date = year?.toString() ?: "" + } + } +} + +@Serializable +data class SimlkSearchResultIds( + @SerialName("simkl_id") + val simklId: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncItem.kt new file mode 100644 index 0000000000..2c86fda952 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncItem.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.data.track.simkl.dto + +import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack +import eu.kanade.tachiyomi.data.track.TrackerManager +import eu.kanade.tachiyomi.data.track.simkl.toTrackStatus +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SimklSyncResult( + val anime: List?, + val tv: List?, + val movies: List?, +) { + fun getFromType(type: String): List? { + return when (type) { + "anime" -> anime + "tv" -> tv + "movies" -> movies + else -> throw Exception("Unknown type: $type") + } + } +} + +@Serializable +data class SimklSyncItem( + val show: SimklSyncResultItem?, + val movie: SimklSyncResultItem?, + @SerialName("total_episodes_count") + val totalEpisodesCount: Long?, + @SerialName("watched_episodes_count") + val watchedEpisodesCount: Double?, + @SerialName("user_rating") + val userRating: Int?, +) { + fun toAnimeTrack(typeName: String, type: String, statusString: String): AnimeTrack { + val resultData = getFromType(typeName) + + return AnimeTrack.create(TrackerManager.SIMKL).apply { + title = resultData.title + remote_id = resultData.ids.simkl + if (typeName != "movie") { + total_episodes = totalEpisodesCount!! + last_episode_seen = watchedEpisodesCount!! + } else { + total_episodes = 1 + last_episode_seen = if (statusString == "completed") 1.0 else 0.0 + } + score = userRating?.toDouble() ?: 0.0 + status = toTrackStatus(statusString) + tracking_url = "/$type/${resultData.ids.simkl}" + } + } + + fun getFromType(typeName: String): SimklSyncResultItem { + return when (typeName) { + "show" -> show!! + "movie" -> movie!! + else -> throw Exception("Unknown type: $typeName") + } + } +} + +@Serializable +data class SimklSyncResultItem( + val title: String, + val ids: SimklSyncResultIds, +) + +@Serializable +data class SimklSyncResultIds( + val simkl: Long, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncWatched.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncWatched.kt new file mode 100644 index 0000000000..1646644aaf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklSyncWatched.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.data.track.simkl.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SimklSyncWatched( + val result: Boolean?, + @SerialName("last_watched") + val lastWatched: String?, + val list: String?, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklUser.kt new file mode 100644 index 0000000000..d04d094a9d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/simkl/dto/SimklUser.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.simkl.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SimklUser( + val account: SimklUserAccount, +) + +@Serializable +data class SimklUserAccount( + val id: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt index 2b7d1ad9c9..2f1dfd0837 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateNotifier.kt @@ -3,12 +3,18 @@ package eu.kanade.tachiyomi.extension import android.content.Context import androidx.core.app.NotificationCompat import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.notify +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get -class ExtensionUpdateNotifier(private val context: Context) { +class ExtensionUpdateNotifier( + private val context: Context, + private val securityPreferences: SecurityPreferences = Injekt.get(), +) { fun promptUpdates(names: List) { context.notify( @@ -22,9 +28,11 @@ class ExtensionUpdateNotifier(private val context: Context) { names.size, ), ) - val extNames = names.joinToString(", ") - setContentText(extNames) - setStyle(NotificationCompat.BigTextStyle().bigText(extNames)) + if (!securityPreferences.hideNotificationContent().get()) { + val extNames = names.joinToString(", ") + setContentText(extNames) + setStyle(NotificationCompat.BigTextStyle().bigText(extNames)) + } setSmallIcon(R.drawable.ic_extension_24dp) setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context)) setAutoCancel(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt index 3fba10fe96..fc87c736cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/InstallStep.kt @@ -1,7 +1,13 @@ package eu.kanade.tachiyomi.extension enum class InstallStep { - Idle, Pending, Downloading, Installing, Installed, Error; + Idle, + Pending, + Downloading, + Installing, + Installed, + Error, + ; fun isCompleted(): Boolean { return this == Installed || this == Error || this == Idle diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt index 502e85fe3b..a5428791f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt @@ -65,14 +65,14 @@ class AnimeExtensionManager( private val iconMap = mutableMapOf() - private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap()) - val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope) + private val installedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val installedExtensionsFlow = installedExtensionsMapFlow.mapExtensions(scope) - private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap()) - val availableExtensionsFlow = _availableExtensionsMapFlow.mapExtensions(scope) + private val availableExtensionsMapFlow = MutableStateFlow(emptyMap()) + val availableExtensionsFlow = availableExtensionsMapFlow.mapExtensions(scope) - private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap()) - val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope) + private val untrustedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val untrustedExtensionsFlow = untrustedExtensionsMapFlow.mapExtensions(scope) init { initAnimeExtensions() @@ -82,7 +82,7 @@ class AnimeExtensionManager( private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() fun getAppIconForSource(sourceId: Long): Drawable? { - val pkgName = _installedExtensionsMapFlow.value.values + val pkgName = installedExtensionsMapFlow.value.values .find { ext -> ext.sources.any { it.id == sourceId } } @@ -90,7 +90,7 @@ class AnimeExtensionManager( ?: return null return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { - AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + AnimeExtensionLoader.getAnimeExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!! .loadIcon(context.packageManager) } } @@ -114,11 +114,11 @@ class AnimeExtensionManager( private fun initAnimeExtensions() { val animeextensions = AnimeExtensionLoader.loadExtensions(context) - _installedExtensionsMapFlow.value = animeextensions + installedExtensionsMapFlow.value = animeextensions .filterIsInstance() .associate { it.extension.pkgName to it.extension } - _untrustedExtensionsMapFlow.value = animeextensions + untrustedExtensionsMapFlow.value = animeextensions .filterIsInstance() .associate { it.extension.pkgName to it.extension } @@ -126,7 +126,7 @@ class AnimeExtensionManager( } /** - * Finds the available anime extensions in the [api] and updates [_availableExtensionsMapFlow]. + * Finds the available anime extensions in the [api] and updates [availableExtensionsMapFlow]. */ suspend fun findAvailableExtensions() { val extensions: List = try { @@ -139,7 +139,7 @@ class AnimeExtensionManager( enableAdditionalSubLanguages(extensions) - _availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } + availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } updatedInstalledAnimeExtensionsStatuses(extensions) setupAvailableAnimeExtensionsSourcesDataMap(extensions) } @@ -187,7 +187,7 @@ class AnimeExtensionManager( return } - val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap() + val installedExtensionsMap = installedExtensionsMapFlow.value.toMutableMap() var changed = false for ((pkgName, extension) in installedExtensionsMap) { @@ -212,7 +212,7 @@ class AnimeExtensionManager( } } if (changed) { - _installedExtensionsMapFlow.value = installedExtensionsMap + installedExtensionsMapFlow.value = installedExtensionsMap } updatePendingUpdatesCount() } @@ -236,7 +236,7 @@ class AnimeExtensionManager( * @param extension The anime extension to be updated. */ fun updateExtension(extension: AnimeExtension.Installed): Flow { - val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() + val availableExt = availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() return installExtension(availableExt) } @@ -273,11 +273,11 @@ class AnimeExtensionManager( * @param extension the extension to trust */ suspend fun trust(extension: AnimeExtension.Untrusted) { - _untrustedExtensionsMapFlow.value[extension.pkgName] ?: return + untrustedExtensionsMapFlow.value[extension.pkgName] ?: return trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) - _untrustedExtensionsMapFlow.value -= extension.pkgName + untrustedExtensionsMapFlow.value -= extension.pkgName AnimeExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) .let { it as? AnimeLoadResult.Success } @@ -290,7 +290,7 @@ class AnimeExtensionManager( * @param extension The anime extension to be registered. */ private fun registerNewExtension(extension: AnimeExtension.Installed) { - _installedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value += extension } /** @@ -300,7 +300,7 @@ class AnimeExtensionManager( * @param extension The anime extension to be registered. */ private fun registerUpdatedExtension(extension: AnimeExtension.Installed) { - _installedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value += extension } /** @@ -310,8 +310,8 @@ class AnimeExtensionManager( * @param pkgName The package name of the uninstalled application. */ private fun unregisterAnimeExtension(pkgName: String) { - _installedExtensionsMapFlow.value -= pkgName - _untrustedExtensionsMapFlow.value -= pkgName + installedExtensionsMapFlow.value -= pkgName + untrustedExtensionsMapFlow.value -= pkgName } /** @@ -330,8 +330,8 @@ class AnimeExtensionManager( } override fun onExtensionUntrusted(extension: AnimeExtension.Untrusted) { - _installedExtensionsMapFlow.value -= extension.pkgName - _untrustedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value -= extension.pkgName + untrustedExtensionsMapFlow.value += extension updatePendingUpdatesCount() } @@ -357,14 +357,14 @@ class AnimeExtensionManager( availableExtension: AnimeExtension.Available? = null, ): Boolean { val availableExt = availableExtension - ?: _availableExtensionsMapFlow.value[pkgName] + ?: availableExtensionsMapFlow.value[pkgName] ?: return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } private fun updatePendingUpdatesCount() { - val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate } + val pendingUpdateCount = installedExtensionsMapFlow.value.values.count { it.hasUpdate } preferences.animeExtensionUpdatesCount().set(pendingUpdateCount) if (pendingUpdateCount == 0) { ExtensionUpdateNotifier(context).dismiss() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt index eaa6c45e51..a894f3c8e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/installer/InstallerAnime.kt @@ -102,7 +102,7 @@ abstract class InstallerAnime(private val service: Service) { } val nextEntry = queue.first() if (waitingInstall.compareAndSet(null, nextEntry)) { - queue.removeFirst() + queue.removeAt(0) processEntry(nextEntry) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt index 9a9fe13d4c..40c222e225 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt @@ -146,7 +146,7 @@ internal object AnimeExtensionLoader { val path = it.absolutePath pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS) - ?.apply { applicationInfo.fixBasePaths(path) } + ?.apply { applicationInfo!!.fixBasePaths(path) } } ?.filter { isPackageAnExtension(it) } ?.map { AnimeExtensionInfo(packageInfo = it, isShared = false) } @@ -203,7 +203,7 @@ internal object AnimeExtensionLoader { ) ?.takeIf { isPackageAnExtension(it) } ?.let { - it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) + it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath) AnimeExtensionInfo( packageInfo = it, isShared = false, @@ -235,12 +235,11 @@ internal object AnimeExtensionLoader { * @param context The application context. * @param extensionInfo The extension to load. */ - @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount") private suspend fun loadExtension(context: Context, extensionInfo: AnimeExtensionInfo): AnimeLoadResult { val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo - val appInfo = pkgInfo.applicationInfo + val appInfo = pkgInfo.applicationInfo!! val pkgName = pkgInfo.packageName val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ") @@ -318,7 +317,7 @@ internal object AnimeExtensionLoader { val obj = Class.forName( it, false, - fallBackClassLoader + fallBackClassLoader, ).getDeclaredConstructor().newInstance() ) { is AnimeSource -> { @@ -402,7 +401,7 @@ internal object AnimeExtensionLoader { */ private fun getSignatures(pkgInfo: PackageInfo): List? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val signingInfo = pkgInfo.signingInfo + val signingInfo = pkgInfo.signingInfo!! if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt index cd2018434d..1251f7dac3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt @@ -62,14 +62,14 @@ class MangaExtensionManager( private val iconMap = mutableMapOf() - private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap()) - val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope) + private val installedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val installedExtensionsFlow = installedExtensionsMapFlow.mapExtensions(scope) - private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap()) - val availableExtensionsFlow = _availableExtensionsMapFlow.mapExtensions(scope) + private val availableExtensionsMapFlow = MutableStateFlow(emptyMap()) + val availableExtensionsFlow = availableExtensionsMapFlow.mapExtensions(scope) - private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap()) - val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope) + private val untrustedExtensionsMapFlow = MutableStateFlow(emptyMap()) + val untrustedExtensionsFlow = untrustedExtensionsMapFlow.mapExtensions(scope) init { initExtensions() @@ -79,7 +79,7 @@ class MangaExtensionManager( private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() fun getAppIconForSource(sourceId: Long): Drawable? { - val pkgName = _installedExtensionsMapFlow.value.values + val pkgName = installedExtensionsMapFlow.value.values .find { ext -> ext.sources.any { it.id == sourceId } } @@ -87,7 +87,7 @@ class MangaExtensionManager( ?: return null return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { - MangaExtensionLoader.getMangaExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + MangaExtensionLoader.getMangaExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!! .loadIcon(context.packageManager) } } @@ -109,11 +109,11 @@ class MangaExtensionManager( private fun initExtensions() { val extensions = MangaExtensionLoader.loadMangaExtensions(context) - _installedExtensionsMapFlow.value = extensions + installedExtensionsMapFlow.value = extensions .filterIsInstance() .associate { it.extension.pkgName to it.extension } - _untrustedExtensionsMapFlow.value = extensions + untrustedExtensionsMapFlow.value = extensions .filterIsInstance() .associate { it.extension.pkgName to it.extension } @@ -121,7 +121,7 @@ class MangaExtensionManager( } /** - * Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow]. + * Finds the available extensions in the [api] and updates [availableExtensionsMapFlow]. */ suspend fun findAvailableExtensions() { val extensions: List = try { @@ -134,7 +134,7 @@ class MangaExtensionManager( enableAdditionalSubLanguages(extensions) - _availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } + availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } updatedInstalledExtensionsStatuses(extensions) setupAvailableExtensionsSourcesDataMap(extensions) } @@ -182,7 +182,7 @@ class MangaExtensionManager( return } - val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap() + val installedExtensionsMap = installedExtensionsMapFlow.value.toMutableMap() var changed = false for ((pkgName, extension) in installedExtensionsMap) { @@ -207,7 +207,7 @@ class MangaExtensionManager( } } if (changed) { - _installedExtensionsMapFlow.value = installedExtensionsMap + installedExtensionsMapFlow.value = installedExtensionsMap } updatePendingUpdatesCount() } @@ -231,7 +231,7 @@ class MangaExtensionManager( * @param extension The extension to be updated. */ fun updateExtension(extension: MangaExtension.Installed): Flow { - val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() + val availableExt = availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() return installExtension(availableExt) } @@ -268,11 +268,11 @@ class MangaExtensionManager( * @param extension the extension to trust */ suspend fun trust(extension: MangaExtension.Untrusted) { - _untrustedExtensionsMapFlow.value[extension.pkgName] ?: return + untrustedExtensionsMapFlow.value[extension.pkgName] ?: return trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) - _untrustedExtensionsMapFlow.value -= extension.pkgName + untrustedExtensionsMapFlow.value -= extension.pkgName MangaExtensionLoader.loadMangaExtensionFromPkgName(context, extension.pkgName) .let { it as? MangaLoadResult.Success } @@ -285,7 +285,7 @@ class MangaExtensionManager( * @param extension The extension to be registered. */ private fun registerNewExtension(extension: MangaExtension.Installed) { - _installedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value += extension } /** @@ -295,7 +295,7 @@ class MangaExtensionManager( * @param extension The extension to be registered. */ private fun registerUpdatedExtension(extension: MangaExtension.Installed) { - _installedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value += extension } /** @@ -305,8 +305,8 @@ class MangaExtensionManager( * @param pkgName The package name of the uninstalled application. */ private fun unregisterExtension(pkgName: String) { - _installedExtensionsMapFlow.value -= pkgName - _untrustedExtensionsMapFlow.value -= pkgName + installedExtensionsMapFlow.value -= pkgName + untrustedExtensionsMapFlow.value -= pkgName } /** @@ -325,8 +325,8 @@ class MangaExtensionManager( } override fun onExtensionUntrusted(extension: MangaExtension.Untrusted) { - _installedExtensionsMapFlow.value -= extension.pkgName - _untrustedExtensionsMapFlow.value += extension + installedExtensionsMapFlow.value -= extension.pkgName + untrustedExtensionsMapFlow.value += extension updatePendingUpdatesCount() } @@ -352,14 +352,14 @@ class MangaExtensionManager( availableExtension: MangaExtension.Available? = null, ): Boolean { val availableExt = availableExtension - ?: _availableExtensionsMapFlow.value[pkgName] + ?: availableExtensionsMapFlow.value[pkgName] ?: return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) } private fun updatePendingUpdatesCount() { - val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate } + val pendingUpdateCount = installedExtensionsMapFlow.value.values.count { it.hasUpdate } preferences.mangaExtensionUpdatesCount().set(pendingUpdateCount) if (pendingUpdateCount == 0) { ExtensionUpdateNotifier(context).dismiss() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt index 37c9edbbc6..2c90443abc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/installer/InstallerManga.kt @@ -102,7 +102,7 @@ abstract class InstallerManga(private val service: Service) { } val nextEntry = queue.first() if (waitingInstall.compareAndSet(null, nextEntry)) { - queue.removeFirst() + queue.removeAt(0) processEntry(nextEntry) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt index 92fd6e4663..7eecb7579e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/util/MangaExtensionLoader.kt @@ -154,7 +154,7 @@ internal object MangaExtensionLoader { val path = it.absolutePath pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS) - ?.apply { applicationInfo.fixBasePaths(path) } + ?.apply { applicationInfo!!.fixBasePaths(path) } } ?.filter { isPackageAnExtension(it) } ?.map { MangaExtensionInfo(packageInfo = it, isShared = false) } @@ -211,7 +211,7 @@ internal object MangaExtensionLoader { ) ?.takeIf { isPackageAnExtension(it) } ?.let { - it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) + it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath) MangaExtensionInfo( packageInfo = it, isShared = false, @@ -243,11 +243,10 @@ internal object MangaExtensionLoader { * @param context The application context. * @param extensionInfo The extension to load. */ - @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount") private suspend fun loadMangaExtension(context: Context, extensionInfo: MangaExtensionInfo): MangaLoadResult { val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo - val appInfo = pkgInfo.applicationInfo + val appInfo = pkgInfo.applicationInfo!! val pkgName = pkgInfo.packageName val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter( @@ -325,7 +324,7 @@ internal object MangaExtensionLoader { val obj = Class.forName( it, false, - fallBackClassLoader + fallBackClassLoader, ).getDeclaredConstructor().newInstance() ) { is MangaSource -> { @@ -408,7 +407,7 @@ internal object MangaExtensionLoader { */ private fun getSignatures(pkgInfo: PackageInfo): List? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val signingInfo = pkgInfo.signingInfo + val signingInfo = pkgInfo.signingInfo!! if (signingInfo.hasMultipleSigners()) { signingInfo.apkContentsSigners } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt index c3b94d6395..110ba09eb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt @@ -97,7 +97,8 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser val incognitoModeFlow = preferences.incognitoMode().changes() combine(secureScreenFlow, incognitoModeFlow) { secureScreen, incognitoMode -> secureScreen == SecurityPreferences.SecureScreenMode.ALWAYS || - secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode + secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && + incognitoMode } .onEach(activity.window::setSecureScreen) .launchIn(activity.lifecycleScope) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 6cf9fca9e5..3c1e55cb07 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -28,12 +29,14 @@ import eu.kanade.tachiyomi.ui.browse.manga.migration.sources.migrateMangaSourceT import eu.kanade.tachiyomi.ui.browse.manga.source.mangaSourcesTab import eu.kanade.tachiyomi.ui.main.MainActivity import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -data class BrowseTab( - private val toExtensions: Boolean = false, -) : Tab() { +data object BrowseTab : Tab { override val options: TabOptions @Composable @@ -52,6 +55,12 @@ data class BrowseTab( navigator.push(GlobalAnimeSearchScreen()) } + private val switchToExtensionTabChannel = Channel(1, BufferOverflow.DROP_OLDEST) + + fun showExtension() { + switchToExtensionTabChannel.trySend(Unit) + } + @Composable override fun Content() { val context = LocalContext.current @@ -63,23 +72,31 @@ data class BrowseTab( val animeExtensionsScreenModel = rememberScreenModel { AnimeExtensionsScreenModel() } val animeExtensionsState by animeExtensionsScreenModel.state.collectAsState() + val tabs = persistentListOf( + animeSourcesTab(), + mangaSourcesTab(), + animeExtensionsTab(animeExtensionsScreenModel), + mangaExtensionsTab(mangaExtensionsScreenModel), + migrateAnimeSourceTab(), + migrateMangaSourceTab(), + ) + + val state = rememberPagerState { tabs.size } + TabbedScreen( titleRes = MR.strings.browse, - tabs = persistentListOf( - animeSourcesTab(), - mangaSourcesTab(), - animeExtensionsTab(animeExtensionsScreenModel), - mangaExtensionsTab(mangaExtensionsScreenModel), - migrateAnimeSourceTab(), - migrateMangaSourceTab(), - ), - startIndex = 2.takeIf { toExtensions }, + tabs = tabs, + state = state, mangaSearchQuery = mangaExtensionsState.searchQuery, onChangeMangaSearchQuery = mangaExtensionsScreenModel::search, animeSearchQuery = animeExtensionsState.searchQuery, onChangeAnimeSearchQuery = animeExtensionsScreenModel::search, scrollable = true, ) + LaunchedEffect(Unit) { + switchToExtensionTabChannel.receiveAsFlow() + .collectLatest { state.scrollToPage(1) } + } LaunchedEffect(Unit) { (context as? MainActivity)?.ready = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsScreenModel.kt index 33b04d08fc..e909c1e446 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsScreenModel.kt @@ -41,7 +41,7 @@ class AnimeExtensionsScreenModel( private val getExtensions: GetAnimeExtensionsByType = Injekt.get(), ) : StateScreenModel(State()) { - private var _currentDownloads = MutableStateFlow>(hashMapOf()) + private val currentDownloads = MutableStateFlow>(hashMapOf()) init { val context = Injekt.get() @@ -62,7 +62,8 @@ class AnimeExtensionsScreenModel( it.name.contains(input, ignoreCase = true) || it.baseUrl.contains(input, ignoreCase = true) || it.id == input.toLongOrNull() - } || extension.name.contains(input, ignoreCase = true) + } || + extension.name.contains(input, ignoreCase = true) } is AnimeExtension.Installed -> { extension.sources.any { @@ -76,7 +77,8 @@ class AnimeExtensionsScreenModel( } else { false } - } || extension.name.contains(input, ignoreCase = true) + } || + extension.name.contains(input, ignoreCase = true) } is AnimeExtension.Untrusted -> extension.name.contains( input, @@ -90,7 +92,7 @@ class AnimeExtensionsScreenModel( screenModelScope.launchIO { combine( state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), - _currentDownloads, + currentDownloads, getExtensions.subscribe(), ) { query, downloads, (_updates, _installed, _available, _untrusted) -> val searchQuery = query ?: "" @@ -183,11 +185,11 @@ class AnimeExtensionsScreenModel( } private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) { - _currentDownloads.update { it + Pair(extension.pkgName, installStep) } + currentDownloads.update { it + Pair(extension.pkgName, installStep) } } private fun removeDownloadState(extension: AnimeExtension) { - _currentDownloads.update { it - extension.pkgName } + currentDownloads.update { it - extension.pkgName } } private suspend fun Flow.collectToInstallUpdate(extension: AnimeExtension) = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsTab.kt index ac0eaabb29..baab42b95f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/AnimeExtensionsTab.kt @@ -1,8 +1,15 @@ package eu.kanade.tachiyomi.ui.browse.anime.extension +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.anime.AnimeExtensionScreen @@ -12,6 +19,7 @@ import eu.kanade.presentation.more.settings.screen.browse.AnimeExtensionReposScr import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.ui.browse.anime.extension.details.AnimeExtensionDetailsScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen +import eu.kanade.tachiyomi.util.system.isPackageInstalled import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource @@ -21,7 +29,10 @@ fun animeExtensionsTab( extensionsScreenModel: AnimeExtensionsScreenModel, ): TabContent { val navigator = LocalNavigator.currentOrThrow + val context = LocalContext.current + val state by extensionsScreenModel.state.collectAsState() + var privateExtensionToUninstall by remember { mutableStateOf(null) } return TabContent( titleRes = MR.strings.label_anime_extensions, @@ -51,7 +62,13 @@ fun animeExtensionsTab( is AnimeExtension.Available -> extensionsScreenModel.installExtension( extension, ) - else -> extensionsScreenModel.uninstallExtension(extension) + else -> { + if (context.isPackageInstalled(extension.pkgName)) { + extensionsScreenModel.uninstallExtension(extension) + } else { + privateExtensionToUninstall = extension + } + } } }, onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, @@ -74,6 +91,50 @@ fun animeExtensionsTab( onUpdateExtension = extensionsScreenModel::updateExtension, onRefresh = extensionsScreenModel::findAvailableExtensions, ) + + privateExtensionToUninstall?.let { extension -> + AnimeExtensionUninstallConfirmation( + extensionName = extension.name, + onClickConfirm = { + extensionsScreenModel.uninstallExtension(extension) + }, + onDismissRequest = { + privateExtensionToUninstall = null + }, + ) + } + }, + ) +} + +@Composable +private fun AnimeExtensionUninstallConfirmation( + extensionName: String, + onClickConfirm: () -> Unit, + onDismissRequest: () -> Unit, +) { + AlertDialog( + title = { + Text(text = stringResource(MR.strings.ext_confirm_remove)) + }, + text = { + Text(text = stringResource(MR.strings.remove_private_extension_message, extensionName)) + }, + confirmButton = { + TextButton( + onClick = { + onClickConfirm() + onDismissRequest() + }, + ) { + Text(text = stringResource(MR.strings.ext_remove)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } }, + onDismissRequest = onDismissRequest, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt index 5eb0974451..71461593b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt @@ -291,7 +291,7 @@ data class BrowseAnimeSourceScreen( ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, - onEditCategories = { navigator.push(CategoriesTab(false)) }, + onEditCategories = { navigator.push(CategoriesTab) }, onConfirm = { include, _ -> screenModel.changeAnimeFavorite(dialog.anime) screenModel.moveAnimeToCategories(dialog.anime, include) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsScreenModel.kt index 83ca025312..ad0eda9347 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsScreenModel.kt @@ -41,7 +41,7 @@ class MangaExtensionsScreenModel( private val getExtensions: GetMangaExtensionsByType = Injekt.get(), ) : StateScreenModel(State()) { - private var _currentDownloads = MutableStateFlow>(hashMapOf()) + private val currentDownloads = MutableStateFlow>(hashMapOf()) init { val context = Injekt.get() @@ -62,7 +62,8 @@ class MangaExtensionsScreenModel( it.name.contains(input, ignoreCase = true) || it.baseUrl.contains(input, ignoreCase = true) || it.id == input.toLongOrNull() - } || extension.name.contains(input, ignoreCase = true) + } || + extension.name.contains(input, ignoreCase = true) } is MangaExtension.Installed -> { extension.sources.any { @@ -76,7 +77,8 @@ class MangaExtensionsScreenModel( } else { false } - } || extension.name.contains(input, ignoreCase = true) + } || + extension.name.contains(input, ignoreCase = true) } is MangaExtension.Untrusted -> extension.name.contains( input, @@ -90,7 +92,7 @@ class MangaExtensionsScreenModel( screenModelScope.launchIO { combine( state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), - _currentDownloads, + currentDownloads, getExtensions.subscribe(), ) { query, downloads, (_updates, _installed, _available, _untrusted) -> val searchQuery = query ?: "" @@ -184,11 +186,11 @@ class MangaExtensionsScreenModel( } private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) { - _currentDownloads.update { it + Pair(extension.pkgName, installStep) } + currentDownloads.update { it + Pair(extension.pkgName, installStep) } } private fun removeDownloadState(extension: MangaExtension) { - _currentDownloads.update { it - extension.pkgName } + currentDownloads.update { it - extension.pkgName } } private suspend fun Flow.collectToInstallUpdate(extension: MangaExtension) = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsTab.kt index df65393531..0d646d10df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/MangaExtensionsTab.kt @@ -1,8 +1,15 @@ package eu.kanade.tachiyomi.ui.browse.manga.extension +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.manga.MangaExtensionScreen @@ -12,6 +19,7 @@ import eu.kanade.presentation.more.settings.screen.browse.MangaExtensionReposScr import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaExtensionDetailsScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen +import eu.kanade.tachiyomi.util.system.isPackageInstalled import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource @@ -21,7 +29,10 @@ fun mangaExtensionsTab( extensionsScreenModel: MangaExtensionsScreenModel, ): TabContent { val navigator = LocalNavigator.currentOrThrow + val context = LocalContext.current + val state by extensionsScreenModel.state.collectAsState() + var privateExtensionToUninstall by remember { mutableStateOf(null) } return TabContent( titleRes = MR.strings.label_manga_extensions, @@ -47,7 +58,13 @@ fun mangaExtensionsTab( is MangaExtension.Available -> extensionsScreenModel.installExtension( extension, ) - else -> extensionsScreenModel.uninstallExtension(extension) + else -> { + if (context.isPackageInstalled(extension.pkgName)) { + extensionsScreenModel.uninstallExtension(extension) + } else { + privateExtensionToUninstall = extension + } + } } }, onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, @@ -70,6 +87,50 @@ fun mangaExtensionsTab( onUpdateExtension = extensionsScreenModel::updateExtension, onRefresh = extensionsScreenModel::findAvailableExtensions, ) + + privateExtensionToUninstall?.let { extension -> + MangaExtensionUninstallConfirmation( + extensionName = extension.name, + onClickConfirm = { + extensionsScreenModel.uninstallExtension(extension) + }, + onDismissRequest = { + privateExtensionToUninstall = null + }, + ) + } + }, + ) +} + +@Composable +private fun MangaExtensionUninstallConfirmation( + extensionName: String, + onClickConfirm: () -> Unit, + onDismissRequest: () -> Unit, +) { + AlertDialog( + title = { + Text(text = stringResource(MR.strings.ext_confirm_remove)) + }, + text = { + Text(text = stringResource(MR.strings.remove_private_extension_message, extensionName)) + }, + confirmButton = { + TextButton( + onClick = { + onClickConfirm() + onDismissRequest() + }, + ) { + Text(text = stringResource(MR.strings.ext_remove)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } }, + onDismissRequest = onDismissRequest, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt index a142548367..caa25aa3b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt @@ -291,7 +291,10 @@ data class BrowseMangaSourceScreen( ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, - onEditCategories = { navigator.push(CategoriesTab(true)) }, + onEditCategories = { + navigator.push(CategoriesTab) + CategoriesTab.showMangaCategory() + }, onConfirm = { include, _ -> screenModel.changeMangaFavorite(dialog.manga) screenModel.moveMangaToCategories(dialog.manga, include) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoriesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoriesTab.kt index f41d660a0b..fd17a081dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoriesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoriesTab.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.category import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext @@ -21,13 +22,14 @@ import eu.kanade.tachiyomi.ui.category.manga.mangaCategoryTab import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -data class CategoriesTab( - private val isManga: Boolean = false, -) : Tab() { +data object CategoriesTab : Tab { override val options: TabOptions @Composable @@ -41,6 +43,12 @@ data class CategoriesTab( ) } + private val switchToMangaCategoryTabChannel = Channel(1, BufferOverflow.DROP_OLDEST) + + fun showMangaCategory() { + switchToMangaCategoryTabChannel.trySend(Unit) + } + @Composable override fun Content() { val context = LocalContext.current @@ -48,14 +56,22 @@ data class CategoriesTab( val animeCategoryScreenModel = rememberScreenModel { AnimeCategoryScreenModel() } val mangaCategoryScreenModel = rememberScreenModel { MangaCategoryScreenModel() } + val tabs = persistentListOf( + animeCategoryTab(), + mangaCategoryTab(), + ) + + val state = rememberPagerState { tabs.size } + TabbedScreen( titleRes = MR.strings.general_categories, - tabs = persistentListOf( - animeCategoryTab(), - mangaCategoryTab(), - ), - startIndex = 1.takeIf { isManga }, + tabs = tabs, + state = state, ) + LaunchedEffect(Unit) { + switchToMangaCategoryTabChannel.receiveAsFlow() + .collectLatest { state.scrollToPage(1) } + } LaunchedEffect(Unit) { (context as? MainActivity)?.ready = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt index 432181cf57..36709480e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt @@ -74,9 +74,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.i18n.stringResource -data class DownloadsTab( - private val isManga: Boolean = false, -) : Tab() { +data object DownloadsTab : Tab { override val options: TabOptions @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt index 4083d7d77f..bdcb1f8eec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.core.net.toUri +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -92,9 +94,11 @@ class AnimeScreen( val context = LocalContext.current val haptic = LocalHapticFeedback.current val scope = rememberCoroutineScope() - val screenModel = rememberScreenModel { AnimeScreenModel(context, animeId, fromSource) } + val lifecycleOwner = LocalLifecycleOwner.current + val screenModel = + rememberScreenModel { AnimeScreenModel(context, lifecycleOwner.lifecycle, animeId, fromSource) } - val state by screenModel.state.collectAsState() + val state by screenModel.state.collectAsStateWithLifecycle() if (state is AnimeScreenModel.State.Loading) { LoadingScreen() @@ -219,7 +223,7 @@ class AnimeScreen( ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, - onEditCategories = { navigator.push(CategoriesTab(false)) }, + onEditCategories = { navigator.push(CategoriesTab) }, onConfirm = { include, _ -> screenModel.moveAnimeToCategoriesAndAddToLibrary(dialog.anime, include) }, @@ -289,7 +293,7 @@ class AnimeScreen( sm.editCover(context, it) } AnimeCoverDialog( - coverDataProvider = { anime!! }, + anime = anime!!, snackbarHostState = sm.snackbarHostState, isCustomCover = remember(anime) { anime!!.hasCustomCover() }, onShareClick = { sm.shareCover(context) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt index 7fabf4e2ee..87ee2fa851 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt @@ -1,9 +1,12 @@ package eu.kanade.tachiyomi.ui.entries.anime import android.content.Context +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import aniyomi.util.nullIfEmpty import aniyomi.util.trimOrNull import cafe.adriel.voyager.core.model.StateScreenModel @@ -17,6 +20,8 @@ import eu.kanade.domain.entries.anime.model.toSAnime import eu.kanade.domain.items.episode.interactor.SetSeenStatus import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithSource import eu.kanade.domain.track.anime.interactor.AddAnimeTracks +import eu.kanade.domain.track.anime.interactor.TrackEpisode +import eu.kanade.domain.track.model.AutoTrackState import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.presentation.entries.DownloadAction import eu.kanade.presentation.entries.anime.components.EpisodeDownloadAction @@ -37,7 +42,7 @@ import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.util.AniChartApi import eu.kanade.tachiyomi.util.episode.getNextUnseen import eu.kanade.tachiyomi.util.removeCovers -import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes +import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async @@ -51,6 +56,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.domain.items.episode.interactor.FilterEpisodesForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.TriState @@ -93,14 +99,16 @@ import java.util.Calendar import kotlin.math.floor class AnimeScreenModel( - val context: Context, - val animeId: Long, + private val context: Context, + private val lifecycle: Lifecycle, + private val animeId: Long, private val isFromSource: Boolean, private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(), internal val playerPreferences: PlayerPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), + private val trackEpisode: TrackEpisode = Injekt.get(), private val downloadManager: AnimeDownloadManager = Injekt.get(), private val downloadCache: AnimeDownloadCache = Injekt.get(), private val getAnimeAndEpisodes: GetAnimeWithEpisodes = Injekt.get(), @@ -120,6 +128,7 @@ class AnimeScreenModel( private val addTracks: AddAnimeTracks = Injekt.get(), private val setAnimeCategories: SetAnimeCategories = Injekt.get(), private val animeRepository: AnimeRepository = Injekt.get(), + private val filterEpisodesForDownload: FilterEpisodesForDownload = Injekt.get(), internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), // AM (FILE_SIZE) --> @@ -144,6 +153,7 @@ class AnimeScreenModel( val episodeSwipeStartAction = libraryPreferences.swipeEpisodeEndAction().get() val episodeSwipeEndAction = libraryPreferences.swipeEpisodeStartAction().get() + var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead().get() val showNextEpisodeAirTime = trackPreferences.showNextEpisodeAiringTime().get() val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get() @@ -183,6 +193,7 @@ class AnimeScreenModel( downloadCache.changes, downloadManager.queueState, ) { animeAndEpisodes, _, _ -> animeAndEpisodes } + .flowWithLifecycle(lifecycle) .collectLatest { (anime, episodes) -> updateSuccessState { it.copy( @@ -552,6 +563,7 @@ class AnimeScreenModel( downloadManager.statusFlow() .filter { it.anime.id == successState?.anime?.id } .catch { error -> logcat(LogPriority.ERROR, error) } + .flowWithLifecycle(lifecycle) .collect { withUIContext { updateDownloadState(it) @@ -563,6 +575,7 @@ class AnimeScreenModel( downloadManager.progressFlow() .filter { it.anime.id == successState?.anime?.id } .catch { error -> logcat(LogPriority.ERROR, error) } + .flowWithLifecycle(lifecycle) .collect { withUIContext { updateDownloadState(it) @@ -812,13 +825,44 @@ class AnimeScreenModel( * @param seen whether to mark episodes as seen or unseen. */ fun markEpisodesSeen(episodes: List, seen: Boolean) { + toggleAllSelection(false) screenModelScope.launchIO { setSeenStatus.await( seen = seen, episodes = episodes.toTypedArray(), ) + + if (!seen || successState?.hasLoggedInTrackers == false || autoTrackState == AutoTrackState.NEVER) { + return@launchIO + } + + val tracks = getTracks.await(animeId) + val maxEpisodeNumber = episodes.maxOf { it.episodeNumber } + val shouldPromptTrackingUpdate = tracks.any { track -> maxEpisodeNumber > track.lastEpisodeSeen } + + if (!shouldPromptTrackingUpdate) return@launchIO + + if (autoTrackState == AutoTrackState.ALWAYS) { + trackEpisode.await(context, animeId, maxEpisodeNumber) + withUIContext { + context.toast( + context.stringResource(MR.strings.trackers_updated_summary_anime, maxEpisodeNumber.toInt()), + ) + } + return@launchIO + } + + val result = snackbarHostState.showSnackbar( + message = context.stringResource(MR.strings.confirm_tracker_update_anime, maxEpisodeNumber.toInt()), + actionLabel = context.stringResource(MR.strings.action_ok), + duration = SnackbarDuration.Short, + withDismissAction = true, + ) + + if (result == SnackbarResult.ActionPerformed) { + trackEpisode.await(context, animeId, maxEpisodeNumber) + } } - toggleAllSelection(false) } /** @@ -873,15 +917,11 @@ class AnimeScreenModel( private fun downloadNewEpisodes(episodes: List) { screenModelScope.launchNonCancellable { val anime = successState?.anime ?: return@launchNonCancellable - val categories = getCategories.await(anime.id).map { it.id } - if (episodes.isEmpty() || !anime.shouldDownloadNewEpisodes( - categories, - downloadPreferences, - ) - ) { - return@launchNonCancellable + val episodesToDownload = filterEpisodesForDownload.await(anime, episodes) + + if (episodesToDownload.isNotEmpty()) { + downloadEpisodes(episodesToDownload) } - downloadEpisodes(episodes) } } @@ -1083,6 +1123,7 @@ class AnimeScreenModel( val supportedTrackerTracks = animeTracks.filter { it.trackerId in supportedTrackerIds } supportedTrackerTracks.size to supportedTrackers.isNotEmpty() } + .flowWithLifecycle(lifecycle) .distinctUntilChanged() .collectLatest { (trackingCount, hasLoggedInTrackers) -> updateSuccessState { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt index a9052d64ff..1fa6a5630c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/track/AnimeTrackInfoDialog.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ButtonDefaults @@ -28,7 +29,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.StateScreenModel @@ -55,6 +55,7 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone +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.ImmutableList @@ -175,6 +176,7 @@ data class AnimeTrackInfoDialogHomeScreen( ), ) }, + onCopyLink = { context.copyTrackerLink(it) }, ) } @@ -188,6 +190,13 @@ data class AnimeTrackInfoDialogHomeScreen( } } + private fun Context.copyTrackerLink(trackItem: AnimeTrackItem) { + val url = trackItem.track?.remoteUrl ?: return + if (url.isNotBlank()) { + copyToClipboard(url, url) + } + } + private class Model( private val animeId: Long, private val sourceId: Long, @@ -681,11 +690,10 @@ data class TrackServiceSearchScreen( val state by screenModel.state.collectAsState() - var textFieldValue by remember { mutableStateOf(TextFieldValue(initialQuery)) } + val textFieldState = rememberTextFieldState(initialQuery) AnimeTrackerSearch( - query = textFieldValue, - onQueryChange = { textFieldValue = it }, - onDispatchQuery = { screenModel.trackingSearch(textFieldValue.text) }, + state = textFieldState, + onDispatchQuery = { screenModel.trackingSearch(textFieldState.text.toString()) }, queryResult = state.queryResult, selected = state.selected, onSelectedChange = screenModel::updateSelection, @@ -839,7 +847,11 @@ private data class TrackerAnimeRemoveScreen( fun deleteAnimeFromService() { screenModelScope.launchNonCancellable { - (tracker as DeletableAnimeTracker).delete(track) + try { + (tracker as DeletableAnimeTracker).delete(track) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to delete anime entry from service" } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreen.kt index 3d8be91967..ccbc3f8d62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.core.net.toUri +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -86,9 +88,11 @@ class MangaScreen( val context = LocalContext.current val haptic = LocalHapticFeedback.current val scope = rememberCoroutineScope() - val screenModel = rememberScreenModel { MangaScreenModel(context, mangaId, fromSource) } + val lifecycleOwner = LocalLifecycleOwner.current + val screenModel = + rememberScreenModel { MangaScreenModel(context, lifecycleOwner.lifecycle, mangaId, fromSource) } - val state by screenModel.state.collectAsState() + val state by screenModel.state.collectAsStateWithLifecycle() if (state is MangaScreenModel.State.Loading) { LoadingScreen() @@ -188,7 +192,10 @@ class MangaScreen( ChangeCategoryDialog( initialSelection = dialog.initialSelection, onDismissRequest = onDismissRequest, - onEditCategories = { navigator.push(CategoriesTab(true)) }, + onEditCategories = { + navigator.push(CategoriesTab) + CategoriesTab.showMangaCategory() + }, onConfirm = { include, _ -> screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include) }, @@ -261,7 +268,7 @@ class MangaScreen( sm.editCover(context, it) } MangaCoverDialog( - coverDataProvider = { manga!! }, + manga = manga!!, snackbarHostState = sm.snackbarHostState, isCustomCover = remember(manga) { manga!!.hasCustomCover() }, onShareClick = { sm.shareCover(context) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt index a21abdd711..0c56485abe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt @@ -1,11 +1,14 @@ package eu.kanade.tachiyomi.ui.entries.manga import android.content.Context +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.ui.util.fastAny +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import aniyomi.util.nullIfEmpty import aniyomi.util.trimOrNull import cafe.adriel.voyager.core.model.StateScreenModel @@ -23,6 +26,8 @@ import eu.kanade.domain.items.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.items.chapter.interactor.SetReadStatus import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.track.manga.interactor.AddMangaTracks +import eu.kanade.domain.track.manga.interactor.TrackChapter +import eu.kanade.domain.track.model.AutoTrackState import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.presentation.entries.DownloadAction import eu.kanade.presentation.entries.manga.components.ChapterDownloadAction @@ -37,7 +42,7 @@ import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.removeCovers -import eu.kanade.tachiyomi.util.shouldDownloadNewChapters +import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async @@ -51,6 +56,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.domain.items.chapter.interactor.FilterChaptersForDownload import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.TriState @@ -63,7 +69,6 @@ import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.category.manga.interactor.GetMangaCategories import tachiyomi.domain.category.manga.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters @@ -91,14 +96,15 @@ import uy.kohesive.injekt.api.get import kotlin.math.floor class MangaScreenModel( - val context: Context, - val mangaId: Long, + private val context: Context, + private val lifecycle: Lifecycle, + private val mangaId: Long, private val isFromSource: Boolean, - private val downloadPreferences: DownloadPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), - readerPreferences: ReaderPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(), + readerPreferences: ReaderPreferences = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(), + private val trackChapter: TrackChapter = Injekt.get(), private val downloadManager: MangaDownloadManager = Injekt.get(), private val downloadCache: MangaDownloadCache = Injekt.get(), private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(), @@ -121,6 +127,7 @@ class MangaScreenModel( private val addTracks: AddMangaTracks = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(), + private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State.Loading) { @@ -144,6 +151,7 @@ class MangaScreenModel( val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get() val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get() + var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead().get() private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope) @@ -177,6 +185,7 @@ class MangaScreenModel( downloadCache.changes, downloadManager.queueState, ) { mangaAndChapters, _, _ -> mangaAndChapters } + .flowWithLifecycle(lifecycle) .collectLatest { (manga, chapters) -> updateSuccessState { it.copy( @@ -189,6 +198,7 @@ class MangaScreenModel( screenModelScope.launchIO { getExcludedScanlators.subscribe(mangaId) + .flowWithLifecycle(lifecycle) .distinctUntilChanged() .collectLatest { excludedScanlators -> updateSuccessState { @@ -199,6 +209,7 @@ class MangaScreenModel( screenModelScope.launchIO { getAvailableScanlators.subscribe(mangaId) + .flowWithLifecycle(lifecycle) .distinctUntilChanged() .collectLatest { availableScanlators -> updateSuccessState { @@ -560,6 +571,7 @@ class MangaScreenModel( downloadManager.statusFlow() .filter { it.manga.id == successState?.manga?.id } .catch { error -> logcat(LogPriority.ERROR, error) } + .flowWithLifecycle(lifecycle) .collect { withUIContext { updateDownloadState(it) @@ -571,6 +583,7 @@ class MangaScreenModel( downloadManager.progressFlow() .filter { it.manga.id == successState?.manga?.id } .catch { error -> logcat(LogPriority.ERROR, error) } + .flowWithLifecycle(lifecycle) .collect { withUIContext { updateDownloadState(it) @@ -823,13 +836,43 @@ class MangaScreenModel( * @param read whether to mark chapters as read or unread. */ fun markChaptersRead(chapters: List, read: Boolean) { + toggleAllSelection(false) screenModelScope.launchIO { setReadStatus.await( read = read, chapters = chapters.toTypedArray(), ) + + if (!read || successState?.hasLoggedInTrackers == false || autoTrackState == AutoTrackState.NEVER) { + return@launchIO + } + + val tracks = getTracks.await(mangaId) + val maxChapterNumber = chapters.maxOf { it.chapterNumber } + val shouldPromptTrackingUpdate = tracks.any { track -> maxChapterNumber > track.lastChapterRead } + + if (!shouldPromptTrackingUpdate) return@launchIO + + if (autoTrackState == AutoTrackState.ALWAYS) { + trackChapter.await(context, mangaId, maxChapterNumber) + withUIContext { + context.toast( + context.stringResource(MR.strings.trackers_updated_summary_manga, maxChapterNumber.toInt()), + ) + } + return@launchIO + } + + val result = snackbarHostState.showSnackbar( + message = context.stringResource(MR.strings.confirm_tracker_update, maxChapterNumber.toInt()), + actionLabel = context.stringResource(MR.strings.action_ok), + duration = SnackbarDuration.Short, + withDismissAction = true, + ) + if (result == SnackbarResult.ActionPerformed) { + trackChapter.await(context, mangaId, maxChapterNumber) + } } - toggleAllSelection(false) } /** @@ -880,15 +923,11 @@ class MangaScreenModel( private fun downloadNewChapters(chapters: List) { screenModelScope.launchNonCancellable { val manga = successState?.manga ?: return@launchNonCancellable - val categories = getCategories.await(manga.id).map { it.id } - if (chapters.isEmpty() || !manga.shouldDownloadNewChapters( - categories, - downloadPreferences, - ) - ) { - return@launchNonCancellable + val chaptersToDownload = filterChaptersForDownload.await(manga, chapters) + + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) } - downloadChapters(chapters) } } @@ -1097,6 +1136,7 @@ class MangaScreenModel( val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds } supportedTrackerTracks.size to supportedTrackers.isNotEmpty() } + .flowWithLifecycle(lifecycle) .distinctUntilChanged() .collectLatest { (trackingCount, hasLoggedInTrackers) -> updateSuccessState { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt index 53a2c4fa1e..c794030c7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/track/MangaTrackInfoDialog.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ButtonDefaults @@ -28,7 +29,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.StateScreenModel @@ -55,6 +55,7 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone +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.ImmutableList @@ -175,6 +176,7 @@ data class MangaTrackInfoDialogHomeScreen( ), ) }, + onCopyLink = { context.copyTrackerLink(it) }, ) } @@ -188,6 +190,13 @@ data class MangaTrackInfoDialogHomeScreen( } } + private fun Context.copyTrackerLink(trackItem: MangaTrackItem) { + val url = trackItem.track?.remoteUrl ?: return + if (url.isNotBlank()) { + copyToClipboard(url, url) + } + } + private class Model( private val mangaId: Long, private val sourceId: Long, @@ -681,11 +690,10 @@ data class TrackServiceSearchScreen( val state by screenModel.state.collectAsState() - var textFieldValue by remember { mutableStateOf(TextFieldValue(initialQuery)) } + val textFieldState = rememberTextFieldState(initialQuery) MangaTrackerSearch( - query = textFieldValue, - onQueryChange = { textFieldValue = it }, - onDispatchQuery = { screenModel.trackingSearch(textFieldValue.text) }, + state = textFieldState, + onDispatchQuery = { screenModel.trackingSearch(textFieldState.text.toString()) }, queryResult = state.queryResult, selected = state.selected, onSelectedChange = screenModel::updateSelection, @@ -839,7 +847,11 @@ private data class TrackerMangaRemoveScreen( fun deleteMangaFromService() { screenModelScope.launchNonCancellable { - (tracker as DeletableMangaTracker).delete(track) + try { + (tracker as DeletableMangaTracker).delete(track) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to delete manga entry from service" } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt index 2e1bf41f11..eaa31ce8c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -object HistoriesTab : Tab() { +data object HistoriesTab : Tab { override val options: TabOptions @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index 94cbdf364a..97dacd29ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -71,8 +71,8 @@ object HomeScreen : Screen() { private val openTabEvent = Channel() private val showBottomNavEvent = Channel() - private const val TabFadeDuration = 200 - private const val TabNavigatorKey = "HomeTabs" + private const val TAB_FADE_DURATION = 200 + private const val TAB_NAVIGATOR_KEY = "HomeTabs" private val uiPreferences: UiPreferences by injectLazy() private val defaultTab = uiPreferences.startScreen().get().tab @@ -84,7 +84,7 @@ object HomeScreen : Screen() { val navigator = LocalNavigator.currentOrThrow TabNavigator( tab = defaultTab, - key = TabNavigatorKey, + key = TAB_NAVIGATOR_KEY, ) { tabNavigator -> // Provide usable navigator to content screen CompositionLocalProvider(LocalNavigator provides navigator) { @@ -128,9 +128,9 @@ object HomeScreen : Screen() { transitionSpec = { materialFadeThroughIn( initialScale = 1f, - durationMillis = TabFadeDuration, + durationMillis = TAB_FADE_DURATION, ) togetherWith - materialFadeThroughOut(durationMillis = TabFadeDuration) + materialFadeThroughOut(durationMillis = TAB_FADE_DURATION) }, label = "tabContent", ) { @@ -173,7 +173,12 @@ object HomeScreen : Screen() { is Tab.Library -> MangaLibraryTab is Tab.Updates -> UpdatesTab is Tab.History -> HistoriesTab - is Tab.Browse -> BrowseTab(it.toExtensions) + is Tab.Browse -> { + if (it.toExtensions) { + BrowseTab.showExtension() + } + BrowseTab + } is Tab.More -> MoreTab } @@ -184,7 +189,7 @@ object HomeScreen : Screen() { navigator.push(MangaScreen(it.mangaIdToOpen)) } if (it is Tab.More && it.toDownloads) { - navigator.push(DownloadsTab()) + navigator.push(DownloadsTab) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt index 6262921ecd..73545c53ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt @@ -79,7 +79,7 @@ import tachiyomi.source.local.entries.anime.LocalAnimeSource import tachiyomi.source.local.entries.anime.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Collections +import kotlin.random.Random /** * Typealias for the library anime, using the category as keys, and list of anime as values. @@ -139,10 +139,8 @@ class AnimeLibraryScreenModel( .applySort(tracks, sort.takeIf { groupType != AnimeLibraryGroup.BY_DEFAULT }, trackingFilter.keys) .mapValues { (_, value) -> if (searchQuery != null) { - // Filter query value.filter { it.matches(searchQuery) } } else { - // Don't do anything value } } @@ -207,10 +205,6 @@ class AnimeLibraryScreenModel( // SY <-- } - /** - * Applies library filters to the given map of anime. - */ - @Suppress("LongMethod", "CyclomaticComplexMethod") private suspend fun AnimeLibraryMap.applyFilters( trackMap: Map>, trackingFilter: Map, @@ -286,15 +280,10 @@ class AnimeLibraryScreenModel( filterFnTracking(it) } - return this.mapValues { entry -> entry.value.fastFilter(filterFn) } + return mapValues { (_, value) -> value.fastFilter(filterFn) } } - /** - * Applies library sorting to the given map of anime. - */ - @Suppress("LongMethod", "CyclomaticComplexMethod") private fun AnimeLibraryMap.applySort( - // Map> trackMap: Map>, groupSort: AnimeLibrarySort? = null, loggedInTrackerIds: Set, @@ -317,11 +306,11 @@ class AnimeLibraryScreenModel( } } - val sortFn: (AnimeLibraryItem, AnimeLibraryItem) -> Int = { i1, i2 -> + fun AnimeLibrarySort.comparator(): Comparator = Comparator { i1, i2 -> // SY --> val sort = groupSort ?: keys.find { it.id == i1.libraryAnime.category }!!.sort // SY <-- - when (sort.type) { + when (this.type) { AnimeLibrarySort.Type.Alphabetical -> { sortAlphabetically(i1, i2) } @@ -334,8 +323,8 @@ class AnimeLibraryScreenModel( AnimeLibrarySort.Type.UnseenCount -> when { // Ensure unseen content comes first i1.libraryAnime.unseenCount == i2.libraryAnime.unseenCount -> 0 - i1.libraryAnime.unseenCount == 0L -> if (sort.isAscending) 1 else -1 - i2.libraryAnime.unseenCount == 0L -> if (sort.isAscending) -1 else 1 + i1.libraryAnime.unseenCount == 0L -> if (this.isAscending) 1 else -1 + i2.libraryAnime.unseenCount == 0L -> if (this.isAscending) -1 else 1 else -> i1.libraryAnime.unseenCount.compareTo(i2.libraryAnime.unseenCount) } AnimeLibrarySort.Type.TotalEpisodes -> { @@ -356,28 +345,30 @@ class AnimeLibraryScreenModel( item1Score.compareTo(item2Score) } AnimeLibrarySort.Type.AiringTime -> when { - i1.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (sort.isAscending) 1 else -1 - i2.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (sort.isAscending) -1 else 1 + i1.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (this.isAscending) 1 else -1 + i2.libraryAnime.anime.nextEpisodeAiringAt == 0L -> if (this.isAscending) -1 else 1 i1.libraryAnime.unseenCount == i2.libraryAnime.unseenCount -> i1.libraryAnime.anime.nextEpisodeAiringAt.compareTo( i2.libraryAnime.anime.nextEpisodeAiringAt, ) else -> i1.libraryAnime.unseenCount.compareTo(i2.libraryAnime.unseenCount) } + AnimeLibrarySort.Type.Random -> { + error("Why Are We Still Here? Just To Suffer?") + } } } - return this.mapValues { entry -> - // SY --> - val isAscending = groupSort?.isAscending ?: keys.find { it.id == entry.key.id }!!.sort.isAscending - // SY <-- - val comparator = if (isAscending) { - Comparator(sortFn) - } else { - Collections.reverseOrder(sortFn) + return mapValues { (key, value) -> + if (key.sort.type == AnimeLibrarySort.Type.Random) { + return@mapValues value.shuffled(Random(libraryPreferences.randomAnimeSortSeed().get())) } - entry.value.sortedWith(comparator.thenComparator(sortAlphabetically)) + val comparator = key.sort.comparator() + .let { if (key.sort.isAscending) it else it.reversed() } + .thenComparator(sortAlphabetically) + + value.sortedWith(comparator) } } @@ -890,8 +881,7 @@ class AnimeLibraryScreenModel( item.libraryAnime.anime.genre?.distinct() ?: emptyList() } libraryAnime.flatMap { item -> - item.libraryAnime.anime.genre?.distinct()?.map { - genre -> + item.libraryAnime.anime.genre?.distinct()?.map { genre -> Pair(genre, item) } ?: emptyList() }.groupBy({ it.first }, { it.second }).filterValues { it.size > 3 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt index aaa1dbe570..e1c816d92d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibrarySettingsScreenModel.kt @@ -34,7 +34,7 @@ class AnimeLibrarySettingsScreenModel( .stateIn( scope = screenModelScope, started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), - initialValue = trackerManager.loggedInTrackers() + initialValue = trackerManager.loggedInTrackers(), ) // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryTab.kt index 8485bba55b..4002b58825 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryTab.kt @@ -71,7 +71,7 @@ import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.source.local.entries.anime.isLocal import uy.kohesive.injekt.injectLazy -object AnimeLibraryTab : Tab() { +data object AnimeLibraryTab : Tab { @OptIn(ExperimentalAnimationGraphicsApi::class) override val options: TabOptions @@ -288,7 +288,7 @@ object AnimeLibraryTab : Tab() { onDismissRequest = onDismissRequest, onEditCategories = { screenModel.clearSelection() - navigator.push(CategoriesTab(false)) + navigator.push(CategoriesTab) }, onConfirm = { include, exclude -> screenModel.clearSelection() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt index 6a377feb2c..e621847267 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt @@ -79,7 +79,7 @@ import tachiyomi.source.local.entries.manga.LocalMangaSource import tachiyomi.source.local.entries.manga.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Collections +import kotlin.random.Random /** * Typealias for the library manga, using the category as keys, and list of manga as values. @@ -139,10 +139,8 @@ class MangaLibraryScreenModel( .applySort(tracks, sort.takeIf { groupType != MangaLibraryGroup.BY_DEFAULT }, trackingFilter.keys) .mapValues { (_, value) -> if (searchQuery != null) { - // Filter query value.filter { it.matches(searchQuery) } } else { - // Don't do anything value } } @@ -207,10 +205,6 @@ class MangaLibraryScreenModel( // SY <-- } - /** - * Applies library filters to the given map of manga. - */ - @Suppress("LongMethod", "CyclomaticComplexMethod") private suspend fun MangaLibraryMap.applyFilters( trackMap: Map>, trackingFilter: Map, @@ -286,15 +280,10 @@ class MangaLibraryScreenModel( filterFnTracking(it) } - return this.mapValues { entry -> entry.value.fastFilter(filterFn) } + return mapValues { (_, value) -> value.fastFilter(filterFn) } } - /** - * Applies library sorting to the given map of manga. - */ - @Suppress("LongMethod", "CyclomaticComplexMethod") private fun MangaLibraryMap.applySort( - // Map> trackMap: Map>, groupSort: MangaLibrarySort? = null, loggedInTrackerIds: Set, @@ -317,11 +306,11 @@ class MangaLibraryScreenModel( } } - val sortFn: (MangaLibraryItem, MangaLibraryItem) -> Int = { i1, i2 -> + fun MangaLibrarySort.comparator(): Comparator = Comparator { i1, i2 -> // SY --> val sort = groupSort ?: keys.find { it.id == i1.libraryManga.category }!!.sort // SY <-- - when (sort.type) { + when (this.type) { MangaLibrarySort.Type.Alphabetical -> { sortAlphabetically(i1, i2) } @@ -334,8 +323,8 @@ class MangaLibraryScreenModel( MangaLibrarySort.Type.UnreadCount -> when { // Ensure unread content comes first i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0 - i1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1 - i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1 + i1.libraryManga.unreadCount == 0L -> if (this.isAscending) 1 else -1 + i2.libraryManga.unreadCount == 0L -> if (this.isAscending) -1 else 1 else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount) } MangaLibrarySort.Type.TotalChapters -> { @@ -355,20 +344,22 @@ class MangaLibraryScreenModel( val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue item1Score.compareTo(item2Score) } + MangaLibrarySort.Type.Random -> { + error("Why Are We Still Here? Just To Suffer?") + } } } - return this.mapValues { entry -> - // SY --> - val isAscending = groupSort?.isAscending ?: keys.find { it.id == entry.key.id }!!.sort.isAscending - // SY <-- - val comparator = if (isAscending) { - Comparator(sortFn) - } else { - Collections.reverseOrder(sortFn) + return mapValues { (key, value) -> + if (key.sort.type == MangaLibrarySort.Type.Random) { + return@mapValues value.shuffled(Random(libraryPreferences.randomMangaSortSeed().get())) } - entry.value.sortedWith(comparator.thenComparator(sortAlphabetically)) + val comparator = key.sort.comparator() + .let { if (key.sort.isAscending) it else it.reversed() } + .thenComparator(sortAlphabetically) + + value.sortedWith(comparator) } } @@ -883,8 +874,7 @@ class MangaLibraryScreenModel( item.libraryManga.manga.genre?.distinct() ?: emptyList() } libraryManga.flatMap { item -> - item.libraryManga.manga.genre?.distinct()?.map { - genre -> + item.libraryManga.manga.genre?.distinct()?.map { genre -> Pair(genre, item) } ?: emptyList() }.groupBy({ it.first }, { it.second }).filterValues { it.size > 3 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt index 4adf7d5985..7e8d8eb4a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibrarySettingsScreenModel.kt @@ -34,7 +34,7 @@ class MangaLibrarySettingsScreenModel( .stateIn( scope = screenModelScope, started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), - initialValue = trackerManager.loggedInTrackers() + initialValue = trackerManager.loggedInTrackers(), ) // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryTab.kt index e59e25d810..54b86f8fda 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryTab.kt @@ -68,7 +68,7 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.source.local.entries.manga.isLocal -object MangaLibraryTab : Tab() { +data object MangaLibraryTab : Tab { @OptIn(ExperimentalAnimationGraphicsApi::class) override val options: TabOptions @@ -303,7 +303,8 @@ object MangaLibraryTab : Tab() { onDismissRequest = onDismissRequest, onEditCategories = { screenModel.clearSelection() - navigator.push(CategoriesTab(true)) + navigator.push(CategoriesTab) + CategoriesTab.showMangaCategory() }, onConfirm = { include, exclude -> screenModel.clearSelection() 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 8deb8db2b2..b790100b4c 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 @@ -12,23 +12,28 @@ import android.os.Build import android.os.Bundle import android.view.View import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -36,16 +41,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.luminance import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.core.animation.doOnEnd import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.util.Consumer -import androidx.core.view.WindowCompat import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.lifecycle.lifecycleScope @@ -53,7 +58,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior import cafe.adriel.voyager.navigator.currentOrThrow -import com.google.accompanist.systemuicontroller.rememberSystemUiController import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.connections.service.ConnectionsPreferences import eu.kanade.presentation.components.AppStateBanners @@ -120,7 +124,6 @@ import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -import androidx.compose.ui.graphics.Color.Companion as ComposeColor class MainActivity : BaseActivity() { @@ -160,18 +163,14 @@ class MainActivity : BaseActivity() { return } - // Draw edge-to-edge - // TODO: replace with ComponentActivity#enableEdgeToEdge - WindowCompat.setDecorFitsSystemWindows(window, false) - setComposeContent { + val context = LocalContext.current + val incognito by preferences.incognitoMode().collectAsState() val downloadOnly by preferences.downloadedOnly().collectAsState() val indexing by downloadCache.isInitializing.collectAsState() val indexingAnime by animeDownloadCache.isInitializing.collectAsState() - // Set status bar color considering the top app state banner - val systemUiController = rememberSystemUiController() val isSystemInDarkTheme = isSystemInDarkTheme() val statusBarBackgroundColor = when { indexing || indexingAnime -> IndexingBannerBackgroundColor @@ -179,27 +178,13 @@ class MainActivity : BaseActivity() { incognito -> IncognitoModeBannerBackgroundColor else -> MaterialTheme.colorScheme.surface } - LaunchedEffect(systemUiController, statusBarBackgroundColor) { - systemUiController.setStatusBarColor( - color = ComposeColor.Transparent, - darkIcons = statusBarBackgroundColor.luminance() > 0.5, - transformColorForLightContent = { ComposeColor.Black }, - ) - } - - // Set navigation bar color - val context = LocalContext.current - val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) - LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) { - systemUiController.setNavigationBarColor( - color = if (context.isNavigationBarNeedsScrim()) { - navbarScrimColor.copy(alpha = 0.7f) - } else { - ComposeColor.Transparent - }, - darkIcons = !isSystemInDarkTheme, - navigationBarContrastEnforced = false, - transformColorForLightContent = { ComposeColor.Black }, + LaunchedEffect(isSystemInDarkTheme, statusBarBackgroundColor) { + // Draw edge-to-edge and set system bars color to transparent + val lightStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK) + val darkStyle = SystemBarStyle.dark(Color.TRANSPARENT) + enableEdgeToEdge( + statusBarStyle = if (statusBarBackgroundColor.luminance() > 0.5) lightStyle else darkStyle, + navigationBarStyle = if (isSystemInDarkTheme) darkStyle else lightStyle, ) } @@ -236,13 +221,25 @@ class MainActivity : BaseActivity() { contentWindowInsets = scaffoldInsets, ) { contentPadding -> // Consume insets already used by app state banners - Box( - modifier = Modifier - .padding(contentPadding) - .consumeWindowInsets(contentPadding), - ) { + Box { // Shows current screen - DefaultNavigatorScreenTransition(navigator = navigator) + DefaultNavigatorScreenTransition( + navigator = navigator, + modifier = Modifier + .padding(contentPadding) + .consumeWindowInsets(contentPadding), + ) + // Draw navigation bar scrim when needed + if (remember { isNavigationBarNeedsScrim() }) { + Spacer( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .windowInsetsBottomHeight(WindowInsets.navigationBars) + .alpha(0.8f) + .background(MaterialTheme.colorScheme.surfaceContainer), + ) + } } } @@ -352,12 +349,13 @@ class MainActivity : BaseActivity() { @Composable private fun HandleOnNewIntent(context: Context, navigator: Navigator) { LaunchedEffect(Unit) { - callbackFlow { + callbackFlow { val componentActivity = context as ComponentActivity val consumer = Consumer { trySend(it) } componentActivity.addOnNewIntentListener(consumer) awaitClose { componentActivity.removeOnNewIntentListener(consumer) } - }.collectLatest { handleIntentAction(it, navigator) } + } + .collectLatest { handleIntentAction(it, navigator) } } } @@ -414,6 +412,7 @@ class MainActivity : BaseActivity() { * When custom animation is used, status and navigation bar color will be set to transparent and will be restored * after the animation is finished. */ + @Suppress("Deprecation") private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) { val root = findViewById(android.R.id.content) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index 3a185d2dec..f343328f2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -44,7 +44,7 @@ import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -object MoreTab : Tab() { +data object MoreTab : Tab { override val options: TabOptions @Composable @@ -78,10 +78,10 @@ object MoreTab : Tab() { isFDroid = context.isInstalledFromFDroid(), navStyle = navStyle, onClickAlt = { navigator.push(navStyle.moreTab) }, - onClickDownloadQueue = { navigator.push(DownloadsTab()) }, - onClickCategories = { navigator.push(CategoriesTab()) }, - onClickStats = { navigator.push(StatsTab()) }, - onClickStorage = { navigator.push(StorageTab()) }, + onClickDownloadQueue = { navigator.push(DownloadsTab) }, + onClickCategories = { navigator.push(CategoriesTab) }, + onClickStats = { navigator.push(StatsTab) }, + onClickStorage = { navigator.push(StorageTab) }, onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) }, onClickSettings = { navigator.push(SettingsScreen()) }, onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) }, @@ -106,10 +106,10 @@ private class MoreScreenModel( var downloadedOnly by preferences.downloadedOnly().asState(screenModelScope) var incognitoMode by preferences.incognitoMode().asState(screenModelScope) - private var _state: MutableStateFlow = MutableStateFlow( + private var _downloadQueueState: MutableStateFlow = MutableStateFlow( DownloadQueueState.Stopped, ) - val downloadQueueState: StateFlow = _state.asStateFlow() + val downloadQueueState: StateFlow = _downloadQueueState.asStateFlow() init { // Handle running/paused status change and queue progress updating @@ -132,7 +132,7 @@ private class MoreScreenModel( val isDownloading = isDownloadingAnime || isDownloadingManga val downloadQueueSize = mangaDownloadQueueSize + animeDownloadQueueSize val pendingDownloadExists = downloadQueueSize != 0 - _state.value = when { + _downloadQueueState.value = when { !pendingDownloadExists -> DownloadQueueState.Stopped !isDownloading -> DownloadQueueState.Paused(downloadQueueSize) else -> DownloadQueueState.Downloading(downloadQueueSize) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/CastOptionsProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/CastOptionsProvider.kt index 22af877e12..3deef90bd1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/CastOptionsProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/CastOptionsProvider.kt @@ -25,7 +25,7 @@ open class CastOptionsProvider : OptionsProvider { private fun getReceiverApplicationId(context: Context): String { return try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_META_DATA) - packageInfo.applicationInfo.metaData.getString(context.getString(R.string.app_cast_id)) + packageInfo.applicationInfo?.metaData?.getString(context.getString(R.string.app_cast_id)) ?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID } catch (e: PackageManager.NameNotFoundException) { CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt index fc771462df..2ffa0adc8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/ExternalIntents.kt @@ -192,7 +192,7 @@ class ExternalIntents { */ private fun getIntentForPackage(pkgName: String, context: Context, uri: Uri, video: Video): Intent { return when (pkgName) { - WebVideoCaster -> webVideoCasterIntent(pkgName, context, uri, video) + WEB_VIDEO_CASTER -> webVideoCasterIntent(pkgName, context, uri, video) else -> standardIntentForPackage(pkgName, context, uri, video) } } @@ -200,7 +200,7 @@ class ExternalIntents { private fun webVideoCasterIntent(pkgName: String, context: Context, uri: Uri, video: Video): Intent { return Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, "video/*") - if (isPackageInstalled(pkgName, context.packageManager)) setPackage(WebVideoCaster) + if (isPackageInstalled(pkgName, context.packageManager)) setPackage(WEB_VIDEO_CASTER) addExtrasAndFlags(true, this) val headers = Bundle() @@ -272,9 +272,9 @@ class ExternalIntents { @Suppress("MagicNumber") private suspend fun torrentIntentForPackage(context: Context, uri: Uri, video: Video): Intent { return Intent(Intent.ACTION_VIEW).apply { - if (isPackageInstalled(Amnis, context.packageManager)) { + if (isPackageInstalled(AMNIS, context.packageManager)) { if (uri.toString().startsWith("magnet:")) { - component = getComponent(Amnis) + component = getComponent(AMNIS) } } else { withUIContext { @@ -347,18 +347,18 @@ class ExternalIntents { */ private fun getComponent(packageName: String): ComponentName? { return when (packageName) { - MpvPlayer -> ComponentName(packageName, "$packageName.MPVActivity") - MxPlayer, MxPlayerFree, MxPlayerPro -> ComponentName( + MPV_PLAYER -> ComponentName(packageName, "$packageName.MPVActivity") + MX_PLAYER, MX_PLAYER_FREE, MX_PLAYER_PRO -> ComponentName( packageName, "$packageName.ActivityScreen", ) - VlcPlayer -> ComponentName(packageName, "$packageName.gui.video.VideoPlayerActivity") - MpvKt, MpvKtPreview -> ComponentName(packageName, "live.mehiz.mpvkt.ui.player.PlayerActivity") - MpvRemote -> ComponentName(packageName, "$packageName.MainActivity") - JustPlayer -> ComponentName(packageName, "$packageName.PlayerActivity") - NextPlayer -> ComponentName(packageName, "$packageName.feature.player.PlayerActivity") - XPlayer -> ComponentName(packageName, "com.inshot.xplayer.activities.PlayerActivity") - Amnis -> ComponentName(packageName, "$packageName.gui.player.PlayerActivity") + VLC_PLAYER -> ComponentName(packageName, "$packageName.gui.video.VideoPlayerActivity") + MPV_KT, MPV_KT_PREVIEW -> ComponentName(packageName, "live.mehiz.mpvkt.ui.player.PlayerActivity") + MPV_REMOTE -> ComponentName(packageName, "$packageName.MainActivity") + JUST_PLAYER -> ComponentName(packageName, "$packageName.PlayerActivity") + NEXT_PLAYER -> ComponentName(packageName, "$packageName.feature.player.PlayerActivity") + X_PLAYER -> ComponentName(packageName, "com.inshot.xplayer.activities.PlayerActivity") + AMNIS -> ComponentName(packageName, "$packageName.gui.player.PlayerActivity") else -> null } } @@ -548,8 +548,10 @@ class ExternalIntents { getTracks.await(anime.id) .mapNotNull { track -> val tracker = trackerManager.get(track.trackerId) - if (tracker != null && tracker.isLoggedIn && - tracker is AnimeTracker && episodeNumber > track.lastEpisodeSeen + if (tracker != null && + tracker.isLoggedIn && + tracker is AnimeTracker && + episodeNumber > track.lastEpisodeSeen ) { val updatedTrack = track.copy(lastEpisodeSeen = episodeNumber) @@ -611,16 +613,16 @@ class ExternalIntents { } // List of supported external players and their packages -const val MpvPlayer = "is.xyz.mpv" -const val MxPlayer = "com.mxtech.videoplayer" -const val MxPlayerFree = "com.mxtech.videoplayer.ad" -const val MxPlayerPro = "com.mxtech.videoplayer.pro" -const val VlcPlayer = "org.videolan.vlc" -const val MpvKt = "live.mehiz.mpvkt" -const val MpvKtPreview = "live.mehiz.mpvkt.preview" -const val MpvRemote = "com.husudosu.mpvremote" -const val JustPlayer = "com.brouken.player" -const val NextPlayer = "dev.anilbeesetti.nextplayer" -const val XPlayer = "video.player.videoplayer" -const val WebVideoCaster = "com.instantbits.cast.webvideo" -const val Amnis = "com.amnis" +const val MPV_PLAYER = "is.xyz.mpv" +const val MX_PLAYER = "com.mxtech.videoplayer" +const val MX_PLAYER_FREE = "com.mxtech.videoplayer.ad" +const val MX_PLAYER_PRO = "com.mxtech.videoplayer.pro" +const val VLC_PLAYER = "org.videolan.vlc" +const val MPV_KT = "live.mehiz.mpvkt" +const val MPV_KT_PREVIEW = "live.mehiz.mpvkt.preview" +const val MPV_REMOTE = "com.husudosu.mpvremote" +const val JUST_PLAYER = "com.brouken.player" +const val NEXT_PLAYER = "dev.anilbeesetti.nextplayer" +const val X_PLAYER = "video.player.videoplayer" +const val WEB_VIDEO_CASTER = "com.instantbits.cast.webvideo" +const val AMNIS = "com.amnis" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 7c6fadfd10..c296a10b2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -559,18 +559,17 @@ class PlayerActivity : BaseActivity() { IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY), ) - isCastApiAvailable = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + isCastApiAvailable = + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS try { if (isCastApiAvailable) { - - mCastContext = CastContext.getSharedInstance(this) - mCastSession = mCastContext!!.sessionManager.currentCastSession - setupCastListener() - } + mCastContext = CastContext.getSharedInstance(this) + mCastSession = mCastContext!!.sessionManager.currentCastSession + setupCastListener() + } } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Service the google play services not available" } } - } private fun copyAssets(configDir: String) { val assetManager = this.assets @@ -809,7 +808,6 @@ class PlayerActivity : BaseActivity() { verticalScrollLeft(0F) } - @Suppress("ReturnCount") private fun getMaxBrightness(): Float { val powerManager = getSystemService(POWER_SERVICE) as? PowerManager ?: return MAX_BRIGHTNESS val brightnessField = powerManager.javaClass.declaredFields.find { @@ -1003,14 +1001,15 @@ class PlayerActivity : BaseActivity() { } override fun onResume() { - isCastApiAvailable = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + isCastApiAvailable = + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS try { if (isCastApiAvailable) { - mCastContext!!.sessionManager.addSessionManagerListener( - mSessionManagerListener!!, - CastSession::class.java, - ) - isInCastMode = mCastSession != null && mCastSession!!.isConnected + mCastContext!!.sessionManager.addSessionManagerListener( + mSessionManagerListener!!, + CastSession::class.java, + ) + isInCastMode = mCastSession != null && mCastSession!!.isConnected } } catch (_: Exception) { } @@ -1021,13 +1020,14 @@ class PlayerActivity : BaseActivity() { override fun onPause() { viewModel.saveCurrentEpisodeWatchingProgress() - isCastApiAvailable = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + isCastApiAvailable = + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS try { if (isCastApiAvailable) { - mCastContext!!.sessionManager.removeSessionManagerListener( - mSessionManagerListener!!, - CastSession::class.java, - ) + mCastContext!!.sessionManager.removeSessionManagerListener( + mSessionManagerListener!!, + CastSession::class.java, + ) } } catch (_: Exception) { } @@ -2119,10 +2119,10 @@ class PlayerActivity : BaseActivity() { "demuxer-cache-time" -> playerControls.updateBufferPosition(value.toInt()) "time-pos" -> { playerControls.updatePlaybackPos(value.toInt()) - //value is in milliseconds + // value is in milliseconds Log.d("PlayerActivity", "time-pos valuetoInt: ${value.toInt()}") - Log.d("PlayerActivity", "time-pos value: ${value}") - Log.d("PlayerActivity", "time-pos value1000 ${value* 1000}") + Log.d("PlayerActivity", "time-pos value: $value") + Log.d("PlayerActivity", "time-pos value1000 ${value * 1000}") viewModel.viewModelScope.launchUI { aniSkipStuff(value) } updatePlaybackState() } @@ -2205,7 +2205,7 @@ class PlayerActivity : BaseActivity() { // -- CAST -- - private fun setupCastListener() { + private fun setupCastListener() { mSessionManagerListener = object : SessionManagerListener { override fun onSessionEnded(session: CastSession, error: Int) { onApplicationDisconnected() @@ -2272,7 +2272,7 @@ class PlayerActivity : BaseActivity() { return currentVideoList?.getOrNull(0)?.videoUrl!!.let { MediaInfo.Builder(it) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - //agrega varios tipos de videos + // agrega varios tipos de videos .setContentType("video/mp4") .setContentUrl(it) .setMetadata(movieMetadata) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index 0ae1c7e81d..335ac69884 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -168,22 +168,28 @@ class PlayerViewModel @JvmOverloads constructor( ?: error("Requested episode of id $episodeId not found in episode list") val episodesForPlayer = episodes.filterNot { - anime.unseenFilterRaw == Anime.EPISODE_SHOW_SEEN && !it.seen || - anime.unseenFilterRaw == Anime.EPISODE_SHOW_UNSEEN && it.seen || - anime.downloadedFilterRaw == Anime.EPISODE_SHOW_DOWNLOADED && !downloadManager.isEpisodeDownloaded( + anime.unseenFilterRaw == Anime.EPISODE_SHOW_SEEN && + !it.seen || + anime.unseenFilterRaw == Anime.EPISODE_SHOW_UNSEEN && + it.seen || + anime.downloadedFilterRaw == Anime.EPISODE_SHOW_DOWNLOADED && + !downloadManager.isEpisodeDownloaded( it.name, it.scanlator, anime.title, anime.source, ) || - anime.downloadedFilterRaw == Anime.EPISODE_SHOW_NOT_DOWNLOADED && downloadManager.isEpisodeDownloaded( + anime.downloadedFilterRaw == Anime.EPISODE_SHOW_NOT_DOWNLOADED && + downloadManager.isEpisodeDownloaded( it.name, it.scanlator, anime.title, anime.source, ) || - anime.bookmarkedFilterRaw == Anime.EPISODE_SHOW_BOOKMARKED && !it.bookmark || - anime.bookmarkedFilterRaw == Anime.EPISODE_SHOW_NOT_BOOKMARKED && it.bookmark + anime.bookmarkedFilterRaw == Anime.EPISODE_SHOW_BOOKMARKED && + !it.bookmark || + anime.bookmarkedFilterRaw == Anime.EPISODE_SHOW_NOT_BOOKMARKED && + it.bookmark }.toMutableList() if (episodesForPlayer.all { it.id != episodeId }) { @@ -324,10 +330,11 @@ class PlayerViewModel @JvmOverloads constructor( fun isEpisodeOnline(): Boolean? { val anime = currentAnime ?: return null val episode = currentEpisode ?: return null - return currentSource is AnimeHttpSource && !EpisodeLoader.isDownload( - episode.toDomainEpisode()!!, - anime, - ) + return currentSource is AnimeHttpSource && + !EpisodeLoader.isDownload( + episode.toDomainEpisode()!!, + anime, + ) } suspend fun loadEpisode(episodeId: Long?): Pair?, String>? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerSettingsScreenModel.kt index 3b0b5c3e2f..81303891de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerSettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerSettingsScreenModel.kt @@ -136,7 +136,6 @@ class PlayerSettingsScreenModel( PlayerDialog( titleRes = MR.strings.player_reset_subtitles, - hideSystemBars = true, modifier = Modifier .fillMaxWidth(fraction = 0.6F) .padding(MaterialTheme.padding.medium), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/EpisodeListDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/EpisodeListDialog.kt index 1a8983f939..af003a7760 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/EpisodeListDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/EpisodeListDialog.kt @@ -39,7 +39,7 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.VerticalFastScroller -import tachiyomi.presentation.core.components.material.ReadItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import java.time.Instant @@ -131,9 +131,9 @@ private fun EpisodeListItem( var textHeight by remember { mutableStateOf(0) } val bookmarkIcon = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.Bookmark - val bookmarkAlpha = if (isBookmarked) 1f else ReadItemAlpha + val bookmarkAlpha = if (isBookmarked) 1f else DISABLED_ALPHA val episodeColor = if (isBookmarked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface - val textAlpha = if (episode.seen) ReadItemAlpha else 1f + val textAlpha = if (episode.seen) DISABLED_ALPHA else 1f val textWeight = if (isCurrentEpisode) FontWeight.Bold else FontWeight.Normal val textStyle = if (isCurrentEpisode) FontStyle.Italic else FontStyle.Normal diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/PlayerDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/PlayerDialog.kt index 464f11de42..382c5d810f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/PlayerDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/PlayerDialog.kt @@ -13,8 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.core.view.WindowInsetsControllerCompat -import com.google.accompanist.systemuicontroller.rememberSystemUiController import dev.icerock.moko.resources.StringResource import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.TextButton @@ -27,7 +25,6 @@ import tachiyomi.presentation.core.i18n.stringResource fun PlayerDialog( titleRes: StringResource, modifier: Modifier = Modifier, - hideSystemBars: Boolean = true, onConfirmRequest: (() -> Unit)? = null, onDismissRequest: () -> Unit, content: @Composable (() -> Unit)? = null, @@ -52,13 +49,6 @@ fun PlayerDialog( modifier = Modifier.fillMaxWidth(), tonalElevation = 1.dp, ) { - if (hideSystemBars) { - rememberSystemUiController().apply { - isSystemBarsVisible = false - systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } - Column(modifier = Modifier.padding(16.dp)) { Text( text = stringResource(titleRes), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/SkipIntroLengthDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/SkipIntroLengthDialog.kt index 8595b52ffb..22cdf600c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/SkipIntroLengthDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/dialogs/SkipIntroLengthDialog.kt @@ -24,8 +24,11 @@ fun SkipIntroLengthDialog( PlayerDialog( titleRes = MR.strings.action_change_intro_length, modifier = Modifier.fillMaxWidth(fraction = if (fromPlayer) 0.5F else 0.8F), - hideSystemBars = fromPlayer, - onConfirmRequest = if (fromPlayer) null else { {} }, + onConfirmRequest = if (fromPlayer) { + null + } else { + {} + }, onDismissRequest = { updateSkipIntroLength(newLength.toLong()) onDismissRequest() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/PlayerSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/PlayerSettingsSheet.kt index 95d2cab6ed..3577a5d678 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/PlayerSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/PlayerSettingsSheet.kt @@ -88,7 +88,6 @@ fun PlayerSettingsSheet( } AdaptiveSheet( - hideSystemBars = true, onDismissRequest = onDismissRequest, ) { Column( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/ScreenshotOptionsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/ScreenshotOptionsSheet.kt index 91875aeba4..ea01eafbfc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/ScreenshotOptionsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/ScreenshotOptionsSheet.kt @@ -40,7 +40,6 @@ fun ScreenshotOptionsSheet( val showSubtitles by remember { mutableStateOf(screenModel.preferences.screenshotSubtitles()) } AdaptiveSheet( - hideSystemBars = true, onDismissRequest = onDismissRequest, ) { Column { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/StreamsCatalogSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/StreamsCatalogSheet.kt index ed5f9f9d41..a7e6848cbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/StreamsCatalogSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/StreamsCatalogSheet.kt @@ -65,7 +65,6 @@ fun StreamsCatalogSheet( tabTitles = tabTitles.toImmutableList(), onOverflowMenuClicked = onSettingsClicked, overflowIcon = Icons.Outlined.Settings, - hideSystemBars = true, ) { page -> Column( modifier = Modifier diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/VideoChaptersSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/VideoChaptersSheet.kt index cb0ef10bee..a886789494 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/VideoChaptersSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/VideoChaptersSheet.kt @@ -38,7 +38,6 @@ fun VideoChaptersSheet( var currentTimePosition by remember { mutableStateOf(timePosition) } AdaptiveSheet( - hideSystemBars = true, onDismissRequest = onDismissRequest, ) { Column( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt index bccab3d334..ae53008cbe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt @@ -36,7 +36,7 @@ import tachiyomi.core.common.storage.extension import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.OutlinedNumericChooser -import tachiyomi.presentation.core.components.material.ReadItemAlpha +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState @@ -126,7 +126,7 @@ private fun SubtitleFont( onValueChanged = onSizeChanged, ) - val boldAlpha = if (boldSubtitles) 1f else ReadItemAlpha + val boldAlpha = if (boldSubtitles) 1f else DISABLED_ALPHA Icon( imageVector = Icons.Outlined.FormatBold, contentDescription = null, @@ -136,7 +136,7 @@ private fun SubtitleFont( .clickable(onClick = updateBold), ) - val italicAlpha = if (italicSubtitles) 1f else ReadItemAlpha + val italicAlpha = if (italicSubtitles) 1f else DISABLED_ALPHA Icon( imageVector = Icons.Outlined.FormatItalic, contentDescription = null, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt index 0d032b789d..9507a150f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt @@ -59,7 +59,6 @@ fun SubtitleSettingsSheet( stringResource(MR.strings.player_subtitle_settings_font_tab), stringResource(MR.strings.player_subtitle_settings_color_tab), ), - hideSystemBars = true, ) { page -> Column( modifier = Modifier diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt index 0c6e33b76a..966f106c28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerControlsView.kt @@ -170,7 +170,7 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr binding.episodeListBtn.setOnClickListener { activity.viewModel.showEpisodeList() } if (playerPreferences.enableCast().get()) { - CastButtonFactory.setUpMediaRouteButton(activity.applicationContext, binding.castBtn) + CastButtonFactory.setUpMediaRouteButton(activity.applicationContext, binding.castBtn) } else { binding.castBtn.visibility = View.GONE } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt index d02cbe7ede..80e9b6d2c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt @@ -8,21 +8,28 @@ import tachiyomi.i18n.MR * Results of the set as cover feature. */ enum class SetAsCover { - Success, AddToLibraryFirst, Error + Success, + AddToLibraryFirst, + Error, } /** * Player's inverted playback text handler */ enum class InvertedPlayback { - NONE, POSITION, DURATION + NONE, + POSITION, + DURATION, } /** * Player's Picture-In-Picture state handler */ enum class PipState { - OFF, ON, STARTED; + OFF, + ON, + STARTED, + ; companion object { internal var mode: PipState = OFF @@ -33,7 +40,12 @@ enum class PipState { * Player's Seek state handler */ enum class SeekState { - DOUBLE_TAP, LOCKED, NONE, SCROLL, SEEKBAR; + DOUBLE_TAP, + LOCKED, + NONE, + SCROLL, + SEEKBAR, + ; companion object { internal var mode = NONE @@ -66,7 +78,8 @@ enum class HwDecState(val title: String, val mpvValue: String) { companion object { private val isWSA = Build.MODEL == "Subsystem for Android(TM)" || - Build.BRAND == "Windows" || Build.BOARD == "windows" + Build.BRAND == "Windows" || + Build.BOARD == "windows" internal val defaultHwDec = when { isWSA -> SW diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 2acabf6f7e..a48e5cb9a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.reader import android.annotation.SuppressLint import android.app.Activity import android.app.assist.AssistContent +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -30,6 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService import androidx.core.graphics.ColorUtils import androidx.core.net.toUri import androidx.core.transition.doOnEnd @@ -82,6 +85,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.hasDisplayCutout import eu.kanade.tachiyomi.util.system.isNightMode +import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent @@ -257,6 +261,9 @@ class ReaderActivity : BaseActivity() { is ReaderViewModel.Event.ShareImage -> { onShareImageResult(event.uri, event.page, event.secondPage) } + is ReaderViewModel.Event.CopyImage -> { + onCopyImageResult(event.uri) + } is ReaderViewModel.Event.SetCoverResult -> { onSetAsCoverResult(event.result) } @@ -433,6 +440,7 @@ class ReaderActivity : BaseActivity() { bookmarked = state.bookmarked, onToggleBookmarked = viewModel::toggleChapterBookmark, onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource }, + onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource }, onShare = ::shareChapter.takeIf { isHttpSource }, viewer = state.viewer, @@ -442,7 +450,7 @@ class ReaderActivity : BaseActivity() { enabledPrevious = state.viewerChapters?.prevChapter != null, currentPage = state.currentPage, totalPages = state.totalPages, - onSliderValueChange = { + onPageIndexChange = { isScrollingThroughPages = true moveToPageIndex(it) }, @@ -688,6 +696,12 @@ class ReaderActivity : BaseActivity() { } } + private fun openChapterInBrowser() { + assistUrl?.let { + openInBrowser(it.toUri(), forceDefaultBrowser = false) + } + } + private fun shareChapter() { assistUrl?.let { val intent = it.toUri().toShareIntent(this, type = "text/plain") @@ -897,6 +911,12 @@ class ReaderActivity : BaseActivity() { startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share))) } + private fun onCopyImageResult(uri: Uri) { + val clipboardManager = applicationContext.getSystemService() ?: return + val clipData = ClipData.newUri(applicationContext.contentResolver, "", uri) + clipboardManager.setPrimaryClip(clipData) + } + /** * Called from the presenter when a page is saved or fails. It shows a message or logs the * event depending on the [result]. @@ -1042,12 +1062,13 @@ class ReaderActivity : BaseActivity() { .onEach { if (viewModel.state.value.viewer !is PagerViewer) return@onEach reloadChapters( - !it && when (readerPreferences.pageLayout().get()) { - PagerConfig.PageLayout.DOUBLE_PAGES -> true - PagerConfig.PageLayout.AUTOMATIC -> - resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - else -> false - }, + !it && + when (readerPreferences.pageLayout().get()) { + PagerConfig.PageLayout.DOUBLE_PAGES -> true + PagerConfig.PageLayout.AUTOMATIC -> + resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + else -> false + }, true, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index b584e6b6e0..8832561797 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -180,21 +180,23 @@ class ReaderViewModel @JvmOverloads constructor( (manga.unreadFilterRaw == Manga.CHAPTER_SHOW_UNREAD && it.read) || ( manga.downloadedFilterRaw == - Manga.CHAPTER_SHOW_DOWNLOADED && !downloadManager.isChapterDownloaded( - it.name, - it.scanlator, - manga.title, - manga.source, - ) + Manga.CHAPTER_SHOW_DOWNLOADED && + !downloadManager.isChapterDownloaded( + it.name, + it.scanlator, + manga.title, + manga.source, + ) ) || ( manga.downloadedFilterRaw == - Manga.CHAPTER_SHOW_NOT_DOWNLOADED && downloadManager.isChapterDownloaded( - it.name, - it.scanlator, - manga.title, - manga.source, - ) + Manga.CHAPTER_SHOW_NOT_DOWNLOADED && + downloadManager.isChapterDownloaded( + it.name, + it.scanlator, + manga.title, + manga.source, + ) ) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || (manga.bookmarkedFilterRaw == Manga.CHAPTER_SHOW_NOT_BOOKMARKED && it.bookmark) @@ -885,7 +887,6 @@ class ReaderViewModel @JvmOverloads constructor( } // SY --> - @Suppress("ReturnCount") fun saveImages() { val (firstPage, secondPage) = (state.value.dialog as? Dialog.PageActions ?: return) val viewer = state.value.viewer as? PagerViewer ?: return @@ -920,7 +921,6 @@ class ReaderViewModel @JvmOverloads constructor( } } - @Suppress("LongParameterList", "TooGenericExceptionThrown") private fun saveImages( page1: ReaderPage, page2: ReaderPage, @@ -961,7 +961,7 @@ class ReaderViewModel @JvmOverloads constructor( * get a path to the file and it has to be decompressed somewhere first. Only the last shared * image will be kept so it won't be taking lots of internal disk space. */ - fun shareImage(useExtraPage: Boolean) { + fun shareImage(copyToClipboard: Boolean, useExtraPage: Boolean) { // SY --> val page = if (useExtraPage) { (state.value.dialog as? Dialog.PageActions)?.extraPage @@ -987,7 +987,7 @@ class ReaderViewModel @JvmOverloads constructor( location = Location.Cache, ), ) - eventChannel.send(Event.ShareImage(uri, page)) + eventChannel.send(if (copyToClipboard) Event.CopyImage(uri) else Event.ShareImage(uri, page)) } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) @@ -995,8 +995,7 @@ class ReaderViewModel @JvmOverloads constructor( } // SY --> - @Suppress("ReturnCount") - fun shareImages() { + fun shareImages(copyToClipboard: Boolean) { val (firstPage, secondPage) = (state.value.dialog as? Dialog.PageActions ?: return) val viewer = state.value.viewer as? PagerViewer ?: return val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages) @@ -1020,7 +1019,9 @@ class ReaderViewModel @JvmOverloads constructor( location = Location.Cache, manga = manga, ) - eventChannel.send(Event.ShareImage(uri, firstPage, secondPage)) + eventChannel.send( + if (copyToClipboard) Event.CopyImage(uri) else Event.ShareImage(uri, firstPage, secondPage), + ) } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) @@ -1168,5 +1169,6 @@ class ReaderViewModel @JvmOverloads constructor( val page: ReaderPage, val secondPage: ReaderPage? = null, ) : Event + data class CopyImage(val uri: Uri) : Event } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt index 397ac51bc1..656d1c7975 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import mihon.core.common.archive.ArchiveReader +import mihon.core.archive.ArchiveReader import tachiyomi.core.common.util.system.ImageUtil /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 4009924ba4..5aa15e305f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -7,7 +7,8 @@ import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences -import mihon.core.common.archive.archiveReader +import mihon.core.archive.archiveReader +import mihon.core.archive.epubReader import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -99,7 +100,7 @@ class ChapterLoader( when (format) { is Format.Directory -> DirectoryPageLoader(format.file) is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context)) - is Format.Epub -> EpubPageLoader(format.file.archiveReader(context)) + is Format.Epub -> EpubPageLoader(format.file.epubReader(context)) } } source is HttpSource -> HttpPageLoader(chapter, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index 76c1cf55ac..985feec3a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.source.MangaSource import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import mihon.core.common.archive.archiveReader +import mihon.core.archive.archiveReader import tachiyomi.domain.entries.manga.model.Manga import uy.kohesive.injekt.injectLazy diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 8ace2fdeee..cc43487a86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -2,27 +2,22 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.util.storage.EpubFile -import mihon.core.common.archive.ArchiveReader +import mihon.core.archive.EpubReader /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { - - private val epub = EpubFile(reader) +internal class EpubPageLoader(private val reader: EpubReader) : PageLoader() { override var isLocal: Boolean = true override suspend fun getPages(): List { - return epub.getImagesFromPages() - .mapIndexed { i, path -> - val streamFn = { epub.getInputStream(path)!! } - ReaderPage(i).apply { - stream = streamFn - status = Page.State.READY - } + return reader.getImagesFromPages().mapIndexed { i, path -> + ReaderPage(i).apply { + stream = { reader.getInputStream(path)!! } + status = Page.State.READY } + } } override suspend fun loadPage(page: ReaderPage) { @@ -31,6 +26,6 @@ internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() { override fun recycle() { super.recycle() - epub.close() + reader.close() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index fa968ac803..da873583e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -93,7 +93,9 @@ internal class HttpPageLoader( val imageUrl = page.imageUrl // Check if the image has been deleted - if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache( + if (page.status == Page.State.READY && + imageUrl != null && + !chapterCache.isImageInCache( imageUrl, ) ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index 5befba4f73..02f5681c9f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -166,7 +166,7 @@ class ReaderPreferences( enum class FlashColor { BLACK, WHITE, - WHITE_BLACK + WHITE_BLACK, } enum class TappingInvertMode( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 836f3ad6a3..f771f3b653 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -400,7 +400,9 @@ open class ReaderPageImageView @JvmOverloads constructor( ) enum class ZoomStartPosition { - LEFT, CENTER, RIGHT + LEFT, + CENTER, + RIGHT, } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 81925c3130..a7471bf1c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -32,15 +32,16 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At Data( transition = transition, currChapterDownloaded = transition.from.pageLoader?.isLocal == true, - goingToChapterDownloaded = manga.isLocal() || transition.to?.chapter?.let { goingToChapter -> - downloadManager.isChapterDownloaded( - chapterName = goingToChapter.name, - chapterScanlator = goingToChapter.scanlator, - mangaTitle = manga.ogTitle, - sourceId = manga.source, - skipCache = true, - ) - } ?: false, + goingToChapterDownloaded = manga.isLocal() || + transition.to?.chapter?.let { goingToChapter -> + downloadManager.isChapterDownloaded( + chapterName = goingToChapter.name, + chapterScanlator = goingToChapter.scanlator, + mangaTitle = manga.ogTitle, + sourceId = manga.source, + skipCache = true, + ) + } ?: false, ) } else { null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index fa79f825c5..bb11852871 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -50,7 +50,7 @@ class PagerPageHolder( /** * Loading progress bar to indicate the current progress. */ - private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext) + private var progressIndicator: ReaderProgressIndicator? = null // = ReaderProgressIndicator(readerThemedContext) /** * Error layout to show when the image fails to load. @@ -70,7 +70,6 @@ class PagerPageHolder( private var extraLoadJob: Job? = null init { - addView(progressIndicator) loadJob = scope.launch { loadPageAndProcessStatus(1) } extraLoadJob = scope.launch { loadPageAndProcessStatus(2) } } @@ -87,6 +86,13 @@ class PagerPageHolder( extraLoadJob = null } + private fun initProgressIndicator() { + if (progressIndicator == null) { + progressIndicator = ReaderProgressIndicator(context) + addView(progressIndicator) + } + } + /** * Loads the page and processes changes to the page's status. * @@ -111,7 +117,7 @@ class PagerPageHolder( Page.State.DOWNLOAD_IMAGE -> { setDownloading() page.progressFlow.collectLatest { value -> - progressIndicator.setProgress(value) + progressIndicator?.setProgress(value) } } Page.State.READY -> setImage() @@ -125,7 +131,8 @@ class PagerPageHolder( * Called when the page is queued. */ private fun setQueued() { - progressIndicator.show() + initProgressIndicator() + progressIndicator?.show() removeErrorLayout() } @@ -133,7 +140,8 @@ class PagerPageHolder( * Called when the page is loading. */ private fun setLoading() { - progressIndicator.show() + initProgressIndicator() + progressIndicator?.show() removeErrorLayout() } @@ -141,7 +149,8 @@ class PagerPageHolder( * Called when the page is downloading. */ private fun setDownloading() { - progressIndicator.show() + initProgressIndicator() + progressIndicator?.show() removeErrorLayout() } @@ -151,9 +160,9 @@ class PagerPageHolder( @Suppress("MagicNumber", "LongMethod") private suspend fun setImage() { if (extraPage == null) { - progressIndicator.setProgress(0) + progressIndicator?.setProgress(0) } else { - progressIndicator.setProgress(95) + progressIndicator?.setProgress(95) } val streamFn = page.stream ?: return @@ -223,7 +232,7 @@ class PagerPageHolder( return splitInHalf(imageSource) } val isDoublePage = ImageUtil.isWideImage( - imageSource + imageSource, ) if (!isDoublePage) { return imageSource @@ -236,7 +245,7 @@ class PagerPageHolder( private fun rotateDualPage(imageSource: BufferedSource): BufferedSource { val isDoublePage = ImageUtil.isWideImage( - imageSource + imageSource, ) return if (isDoublePage) { val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f @@ -252,7 +261,7 @@ class PagerPageHolder( "MagicNumber", "LongMethod", "CyclomaticComplexMethod", - "ComplexCondition" + "ComplexCondition", ) private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource { // Handle adding a center margin to wide images if requested @@ -281,7 +290,7 @@ class PagerPageHolder( return imageSource } - scope.launch { progressIndicator.setProgress(96) } + scope.launch { progressIndicator?.setProgress(96) } if (imageBitmap.height < imageBitmap.width) { imageSource2.close() page.fullPage = true @@ -299,7 +308,7 @@ class PagerPageHolder( return imageSource } - scope.launch { progressIndicator.setProgress(97) } + scope.launch { progressIndicator?.setProgress(97) } if (imageBitmap2.height < imageBitmap2.width) { imageSource2.close() extraPage?.fullPage = true @@ -359,9 +368,9 @@ class PagerPageHolder( private fun updateProgress(progress: Int) { scope.launch { if (progress == 100) { - progressIndicator.hide() + progressIndicator?.hide() } else { - progressIndicator.setProgress(progress) + progressIndicator?.setProgress(progress) } } } @@ -398,7 +407,8 @@ class PagerPageHolder( viewer.config.centerMarginType and PagerConfig.CenterMarginType .DOUBLE_PAGE_CENTER_MARGIN ) > 0 && - viewer.config.doublePages && !viewer.config.imageCropBorders + viewer.config.doublePages && + !viewer.config.imageCropBorders ) { 48 } else { @@ -417,13 +427,13 @@ class PagerPageHolder( * Called when the page has an error. */ private fun setError() { - progressIndicator.hide() + progressIndicator?.hide() showErrorLayout() } override fun onImageLoaded() { super.onImageLoaded() - progressIndicator.hide() + progressIndicator?.hide() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index c04b6c1397..60d5a780db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -290,8 +290,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { * Sets the active [chapters] on this pager. */ private fun setChaptersInternal(chapters: ViewerChapters) { - val forceTransition = config.alwaysShowChapterTransition || - adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition + val forceTransition = + config.alwaysShowChapterTransition || + adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition adapter.setChapters(chapters, forceTransition) // Layout the pager once a chapter is being set 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 4c648f70af..d21c2acea6 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 @@ -110,7 +110,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { // Add next chapter transition and pages. nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter) .also { - if (nextHasMissingChapters || forceTransition || + if (nextHasMissingChapters || + forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded ) { newItems.add(it) @@ -307,8 +308,10 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() { if (items[itemIndex]?.fullPage == true || items[itemIndex]?.shiftedPage == true) { // Add a 'blank' page after each full page. It will be used when chunked to solo a page items.add(itemIndex + 1, null) - if (items[itemIndex]?.fullPage == true && itemIndex > 0 && - items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0 + if (items[itemIndex]?.fullPage == true && + itemIndex > 0 && + items[itemIndex - 1] != null && + (itemIndex - 1) % 2 == 0 ) { // If a page is a full page, check if the previous page needs to be isolated // we should check if it's an even or odd page, since even pages need shifting diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt index 63be4007b3..71db61ebe8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt @@ -13,12 +13,7 @@ import androidx.recyclerview.widget.RecyclerView.NO_POSITION * This layout manager uses the same package name as the support library in order to use a package * protected method. */ -class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) { - - /** - * Extra layout space is set to half the screen height. - */ - private val extraLayoutSpace = context.resources.displayMetrics.heightPixels / 2 +class WebtoonLayoutManager(context: Context, private val extraLayoutSpace: Int) : LinearLayoutManager(context) { init { isItemPrefetchEnabled = false @@ -27,6 +22,7 @@ class WebtoonLayoutManager(context: Context) : LinearLayoutManager(context) { /** * Returns the custom extra layout space. */ + @Deprecated("Deprecated in Java") override fun getExtraLayoutSpace(state: RecyclerView.State): Int { return extraLayoutSpace } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt index d02fa774cd..5134f4a29e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt @@ -190,7 +190,11 @@ class WebtoonRecyclerView @JvmOverloads constructor( setScaleRate(currentScale) - layoutParams.height = if (currentScale < 1) { (originalHeight / currentScale).toInt() } else { originalHeight } + layoutParams.height = if (currentScale < 1) { + (originalHeight / currentScale).toInt() + } else { + originalHeight + } halfHeight = layoutParams.height / 2 if (currentScale != DEFAULT_RATE) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index fd6cf26307..9b9745eb78 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -47,10 +47,15 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr */ private val frame = WebtoonFrame(activity) + /** + * Distance to scroll when the user taps on one side of the recycler view. + */ + private val scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4 + /** * Layout manager of the recycler view. */ - private val layoutManager = WebtoonLayoutManager(activity) + private val layoutManager = WebtoonLayoutManager(activity, scrollDistance) /** * Configuration used by this viewer, like allow taps, or crop image borders. @@ -62,11 +67,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr */ private val adapter = WebtoonAdapter(this) - /** - * Distance to scroll when the user taps on one side of the recycler view. - */ - private var scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4 - /** * Currently active item. It can be a chapter page or a chapter transition. */ @@ -79,6 +79,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr .threshold init { + recycler.setItemViewCacheSize(RECYCLER_VIEW_CACHE_SIZE) recycler.isVisible = false // Don't let the recycler layout yet recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) recycler.isFocusable = false @@ -359,3 +360,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr ) } } + +// Double the cache size to reduce rebinds/recycles incurred by the extra layout space on scroll direction changes +private const val RECYCLER_VIEW_CACHE_SIZE = 4 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/connections/DiscordLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/connections/DiscordLoginActivity.kt index e0723d6ee0..faf5521693 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/connections/DiscordLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/connections/DiscordLoginActivity.kt @@ -44,7 +44,7 @@ class DiscordLoginActivity : BaseActivity() { (function() { const wreq = (webpackChunkdiscord_app.push([[''], {}, e => { m = []; for (let c in e.c) m.push(e.c[c])}]), m) webpackChunkdiscord_app.pop() - const token = wreq.find(m => m?.exports?.default?.getToken !== void 0).exports.default.getToken(); + const token = wreq.find(m => m?.exports?.default?.getToken !== void 0).exports.default.getToken(); return token; })() """.trimIndent(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsTab.kt index 79d7ed0bcd..fd729ce8d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsTab.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.stats import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext @@ -18,9 +19,7 @@ import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -data class StatsTab( - private val isManga: Boolean = false, -) : Tab() { +data object StatsTab : Tab { override val options: TabOptions @Composable @@ -38,14 +37,16 @@ data class StatsTab( override fun Content() { val context = LocalContext.current + val tabs = persistentListOf( + animeStatsTab(), + mangaStatsTab(), + ) + val state = rememberPagerState { tabs.size } + TabbedScreen( titleRes = MR.strings.label_stats, - tabs = persistentListOf( - animeStatsTab(), - mangaStatsTab(), - ), - startIndex = 1.takeIf { isManga }, - + tabs = tabs, + state = state, ) LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/storage/StorageTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/storage/StorageTab.kt index 5f9c376a55..1b022c47f6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/storage/StorageTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/storage/StorageTab.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.storage import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext @@ -18,9 +19,7 @@ import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -data class StorageTab( - private val isManga: Boolean = false, -) : Tab() { +data object StorageTab : Tab { override val options: TabOptions @Composable @@ -38,13 +37,16 @@ data class StorageTab( override fun Content() { val context = LocalContext.current + val tabs = persistentListOf( + animeStorageTab(), + mangaStorageTab(), + ) + val state = rememberPagerState { tabs.size } + TabbedScreen( titleRes = MR.strings.label_storage, - tabs = persistentListOf( - animeStorageTab(), - mangaStorageTab(), - ), - startIndex = 1.takeIf { isManga }, + tabs = tabs, + state = state, ) LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index ed87ab64d3..6b1938ebf2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource -object UpdatesTab : Tab() { +data object UpdatesTab : Tab { override val options: TabOptions @Composable @@ -41,7 +41,7 @@ object UpdatesTab : Tab() { ) } override suspend fun onReselect(navigator: Navigator) { - navigator.push(DownloadsTab()) + navigator.push(DownloadsTab) } @Composable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt index 38dca6ac35..af5eadad6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreenModel.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import logcat.LogPriority -import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager @@ -57,7 +57,9 @@ class WebViewScreenModel( } fun clearCookies(url: String) { - val cleared = network.cookieJar.remove(url.toHttpUrl()) - logcat { "Cleared $cleared cookies for: $url" } + url.toHttpUrlOrNull()?.let { + val cleared = network.cookieJar.remove(it) + logcat { "Cleared $cleared cookies for: $url" } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt index 19537616e7..d11df9136a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt @@ -5,7 +5,6 @@ import eu.kanade.domain.entries.anime.model.hasCustomCover import eu.kanade.domain.entries.anime.model.toSAnime import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.data.cache.AnimeCoverCache -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.image.anime.LocalAnimeCoverManager @@ -50,31 +49,6 @@ fun Anime.removeCovers(coverCache: AnimeCoverCache = Injekt.get()): Anime { } } -fun Anime.shouldDownloadNewEpisodes(dbCategories: List, preferences: DownloadPreferences): Boolean { - if (!favorite) return false - - val categories = dbCategories.ifEmpty { listOf(0L) } - - // Boolean to determine if user wants to automatically download new episodes. - val downloadNewEpisodes = preferences.downloadNewEpisodes().get() - if (!downloadNewEpisodes) return false - - val includedCategories = preferences.downloadNewEpisodeCategories().get().map { it.toLong() } - val excludedCategories = preferences.downloadNewEpisodeCategoriesExclude().get().map { it.toLong() } - - // Default: Download from all categories - if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true - - // In excluded category - if (categories.any { it in excludedCategories }) return false - - // Included category not selected - if (includedCategories.isEmpty()) return true - - // In included category - return categories.any { it in includedCategories } -} - suspend fun Anime.editCover( coverManager: LocalAnimeCoverManager, stream: InputStream, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index f55b69bcfd..e7cec78d4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -5,7 +5,6 @@ import eu.kanade.domain.entries.manga.model.hasCustomCover import eu.kanade.domain.entries.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.image.manga.LocalMangaCoverManager @@ -50,31 +49,6 @@ fun Manga.removeCovers(coverCache: MangaCoverCache = Injekt.get()): Manga { } } -fun Manga.shouldDownloadNewChapters(dbCategories: List, preferences: DownloadPreferences): Boolean { - if (!favorite) return false - - val categories = dbCategories.ifEmpty { listOf(0L) } - - // Boolean to determine if user wants to automatically download new chapters. - val downloadNewChapters = preferences.downloadNewChapters().get() - if (!downloadNewChapters) return false - - val includedCategories = preferences.downloadNewChapterCategories().get().map { it.toLong() } - val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() } - - // Default: Download from all categories - if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true - - // In excluded category - if (categories.any { it in excludedCategories }) return false - - // Included category not selected - if (includedCategories.isEmpty()) return true - - // In included category - return categories.any { it in includedCategories } -} - suspend fun Manga.editCover( coverManager: LocalMangaCoverManager, stream: InputStream, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/SubtitleSelect.kt b/app/src/main/java/eu/kanade/tachiyomi/util/SubtitleSelect.kt index 67ed06dd8f..1770332282 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/SubtitleSelect.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/SubtitleSelect.kt @@ -46,9 +46,11 @@ class SubtitleSelect(private val playerPreferences: PlayerPreferences) { private fun containsLang(title: String, locale: Locale): Boolean { val localName = locale.getDisplayName(locale) val englishName = locale.getDisplayName(Locale.ENGLISH).substringBefore(" (") - val langRegex = Regex("""\b${locale.getISO3Language()}\b""", RegexOption.IGNORE_CASE) + val langRegex = Regex("""\b${locale.isO3Language}|${locale.language}\b""", RegexOption.IGNORE_CASE) - return title.contains(localName) || title.contains(englishName) || langRegex.find(title) != null + return title.contains(localName, true) || + title.contains(englishName, true) || + langRegex.find(title) != null } @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt index 9614fb4627..ab36e758a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt @@ -27,7 +27,8 @@ fun Context.notify( } fun Context.notify(id: Int, notification: Notification) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + PermissionChecker.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS, ) != PermissionChecker.PERMISSION_GRANTED @@ -39,7 +40,8 @@ fun Context.notify(id: Int, notification: Notification) { } fun Context.notify(notificationWithIdAndTags: List) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + PermissionChecker.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS, ) != PermissionChecker.PERMISSION_GRANTED diff --git a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt index 801411013a..cfb8db2b66 100644 --- a/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt +++ b/app/src/main/java/mihon/core/migration/MigrationJobFactory.kt @@ -9,21 +9,24 @@ import tachiyomi.core.common.util.system.logcat class MigrationJobFactory( private val migrationContext: MigrationContext, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) { - @SuppressWarnings("MaxLineLength") fun create(migrations: List): Deferred = with(scope) { return migrations.sortedBy { it.version } .fold(CompletableDeferred(true)) { acc: Deferred, migration: Migration -> if (!migrationContext.dryrun) { - logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" } + logcat { + "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" + } async(start = CoroutineStart.UNDISPATCHED) { val prev = acc.await() migration(migrationContext) || prev } } else { - logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" } + logcat { + "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" + } CompletableDeferred(true) } } diff --git a/app/src/main/java/mihon/core/migration/MigrationStrategy.kt b/app/src/main/java/mihon/core/migration/MigrationStrategy.kt index 9fd5f4f919..8033ebc6d6 100644 --- a/app/src/main/java/mihon/core/migration/MigrationStrategy.kt +++ b/app/src/main/java/mihon/core/migration/MigrationStrategy.kt @@ -12,7 +12,7 @@ interface MigrationStrategy { class DefaultMigrationStrategy( private val migrationJobFactory: MigrationJobFactory, private val migrationCompletedListener: MigrationCompletedListener, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) : MigrationStrategy { override operator fun invoke(migrations: List): Deferred = with(scope) { @@ -46,7 +46,7 @@ class NoopMigrationStrategy(val state: Boolean) : MigrationStrategy { class VersionRangeMigrationStrategy( private val versions: IntRange, - private val strategy: DefaultMigrationStrategy + private val strategy: DefaultMigrationStrategy, ) : MigrationStrategy { override operator fun invoke(migrations: List): Deferred { diff --git a/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt b/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt index 7e06fecb3d..0905cc086a 100644 --- a/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt +++ b/app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt @@ -6,15 +6,13 @@ class MigrationStrategyFactory( ) { fun create(old: Int, new: Int): MigrationStrategy { - val versions = (old + 1)..new val strategy = when { old == 0 -> InitialMigrationStrategy( strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), ) - old >= new -> NoopMigrationStrategy(false) else -> VersionRangeMigrationStrategy( - versions = versions, + versions = (old + 1)..new, strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope), ) } diff --git a/app/src/main/java/mihon/core/migration/Migrator.kt b/app/src/main/java/mihon/core/migration/Migrator.kt index 11f22a8c9b..c01a3873e6 100644 --- a/app/src/main/java/mihon/core/migration/Migrator.kt +++ b/app/src/main/java/mihon/core/migration/Migrator.kt @@ -10,14 +10,14 @@ import kotlinx.coroutines.runBlocking object Migrator { private var result: Deferred? = null - val scope = CoroutineScope(Dispatchers.Main + Job()) + val scope = CoroutineScope(Dispatchers.IO + Job()) fun initialize( old: Int, new: Int, migrations: List, dryrun: Boolean = false, - onMigrationComplete: () -> Unit + onMigrationComplete: () -> Unit, ) { val migrationContext = MigrationContext(dryrun) val migrationJobFactory = MigrationJobFactory(migrationContext, scope) diff --git a/app/src/main/java/mihon/core/migration/migrations/EnableAutoBackupMigration.kt b/app/src/main/java/mihon/core/migration/migrations/EnableAutoBackupMigration.kt index 70db002533..3249a29890 100644 --- a/app/src/main/java/mihon/core/migration/migrations/EnableAutoBackupMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/EnableAutoBackupMigration.kt @@ -10,7 +10,6 @@ class EnableAutoBackupMigration : Migration { override val version = 84f // Always attempt automatic backup creation - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val backupPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/MigrateRotationViewerValuesMigration.kt b/app/src/main/java/mihon/core/migration/migrations/MigrateRotationViewerValuesMigration.kt index f364f7ec00..877624cae9 100644 --- a/app/src/main/java/mihon/core/migration/migrations/MigrateRotationViewerValuesMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/MigrateRotationViewerValuesMigration.kt @@ -11,7 +11,6 @@ class MigrateRotationViewerValuesMigration : Migration { override val version = 60f // Migrate Rotation and Viewer values to default values for viewer_flags - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val prefs = PreferenceManager.getDefaultSharedPreferences(context) diff --git a/app/src/main/java/mihon/core/migration/migrations/MigrateSortingModeMigration.kt b/app/src/main/java/mihon/core/migration/migrations/MigrateSortingModeMigration.kt index dba141dd81..41dcf24685 100644 --- a/app/src/main/java/mihon/core/migration/migrations/MigrateSortingModeMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/MigrateSortingModeMigration.kt @@ -11,7 +11,6 @@ class MigrateSortingModeMigration : Migration { override val version = 64f // Switch to sort per category - @Suppress("CyclomaticComplexMethod", "MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val libraryPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/RelativeTimestampMigration.kt b/app/src/main/java/mihon/core/migration/migrations/RelativeTimestampMigration.kt index 036cf2b930..9b8a0a558e 100644 --- a/app/src/main/java/mihon/core/migration/migrations/RelativeTimestampMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/RelativeTimestampMigration.kt @@ -9,7 +9,6 @@ class RelativeTimestampMigration : Migration { override val version = 106f // Bring back simplified relative timestamp setting - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val preferenceStore = migrationContext.get() ?: return false val uiPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/RemoveOneTwoHourUpdateMigration.kt b/app/src/main/java/mihon/core/migration/migrations/RemoveOneTwoHourUpdateMigration.kt index 0c43243670..0787920f57 100644 --- a/app/src/main/java/mihon/core/migration/migrations/RemoveOneTwoHourUpdateMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/RemoveOneTwoHourUpdateMigration.kt @@ -11,7 +11,6 @@ class RemoveOneTwoHourUpdateMigration : Migration { override val version = 61f // Handle removed every 1 or 2 hour library updates - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val libraryPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/RemoveQuickUpdateMigration.kt b/app/src/main/java/mihon/core/migration/migrations/RemoveQuickUpdateMigration.kt index 7d551546c8..06f1b10dd1 100644 --- a/app/src/main/java/mihon/core/migration/migrations/RemoveQuickUpdateMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/RemoveQuickUpdateMigration.kt @@ -11,7 +11,6 @@ class RemoveQuickUpdateMigration : Migration { override val version = 71f // Handle removed every 3, 4, 6, and 8 hour library updates - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val libraryPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/RemoveReaderTapMigration.kt b/app/src/main/java/mihon/core/migration/migrations/RemoveReaderTapMigration.kt index ea6ad7139b..0a11529282 100644 --- a/app/src/main/java/mihon/core/migration/migrations/RemoveReaderTapMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/RemoveReaderTapMigration.kt @@ -10,7 +10,6 @@ class RemoveReaderTapMigration : Migration { override val version = 77f // Remove reader tapping option in favor of disabled nav layouts - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val readerPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/ResetSortPreferenceRemovedMigration.kt b/app/src/main/java/mihon/core/migration/migrations/ResetSortPreferenceRemovedMigration.kt index 89df599ecb..fdbca17351 100644 --- a/app/src/main/java/mihon/core/migration/migrations/ResetSortPreferenceRemovedMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/ResetSortPreferenceRemovedMigration.kt @@ -11,7 +11,6 @@ class ResetSortPreferenceRemovedMigration : Migration { override val version = 44f // Reset sorting preference if using removed sort by source - @Suppress("MagicNumber") override suspend fun invoke(migrationContext: MigrationContext): Boolean { val context = migrationContext.get() ?: return false val libraryPreferences = migrationContext.get() ?: return false diff --git a/app/src/main/java/mihon/core/migration/migrations/SetupAnimeLibraryUpdateMigration.kt b/app/src/main/java/mihon/core/migration/migrations/SetupAnimeLibraryUpdateMigration.kt index 73e5d43809..67fe4495fc 100644 --- a/app/src/main/java/mihon/core/migration/migrations/SetupAnimeLibraryUpdateMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/SetupAnimeLibraryUpdateMigration.kt @@ -1,6 +1,6 @@ package mihon.core.migration.migrations -import eu.kanade.tachiyomi.App +import android.app.Application import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateJob import mihon.core.migration.Migration import mihon.core.migration.MigrationContext @@ -9,7 +9,7 @@ class SetupAnimeLibraryUpdateMigration : Migration { override val version: Float = Migration.ALWAYS override suspend fun invoke(migrationContext: MigrationContext): Boolean { - val context = migrationContext.get() ?: return false + val context = migrationContext.get() ?: return false AnimeLibraryUpdateJob.setupTask(context) diff --git a/app/src/main/java/mihon/core/migration/migrations/SetupBackupCreateMigration.kt b/app/src/main/java/mihon/core/migration/migrations/SetupBackupCreateMigration.kt index 8b637a3263..f5361d9c49 100644 --- a/app/src/main/java/mihon/core/migration/migrations/SetupBackupCreateMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/SetupBackupCreateMigration.kt @@ -1,6 +1,6 @@ package mihon.core.migration.migrations -import eu.kanade.tachiyomi.App +import android.app.Application import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import mihon.core.migration.Migration import mihon.core.migration.MigrationContext @@ -9,7 +9,7 @@ class SetupBackupCreateMigration : Migration { override val version: Float = Migration.ALWAYS override suspend fun invoke(migrationContext: MigrationContext): Boolean { - val context = migrationContext.get() ?: return false + val context = migrationContext.get() ?: return false BackupCreateJob.setupTask(context) diff --git a/app/src/main/java/mihon/core/migration/migrations/SetupMangaLibraryUpdateMigration.kt b/app/src/main/java/mihon/core/migration/migrations/SetupMangaLibraryUpdateMigration.kt index 6df1c138ce..3620fcc2a4 100644 --- a/app/src/main/java/mihon/core/migration/migrations/SetupMangaLibraryUpdateMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/SetupMangaLibraryUpdateMigration.kt @@ -1,6 +1,6 @@ package mihon.core.migration.migrations -import eu.kanade.tachiyomi.App +import android.app.Application import eu.kanade.tachiyomi.data.library.manga.MangaLibraryUpdateJob import mihon.core.migration.Migration import mihon.core.migration.MigrationContext @@ -9,7 +9,7 @@ class SetupMangaLibraryUpdateMigration : Migration { override val version: Float = Migration.ALWAYS override suspend fun invoke(migrationContext: MigrationContext): Boolean { - val context = migrationContext.get() ?: return false + val context = migrationContext.get() ?: return false MangaLibraryUpdateJob.setupTask(context) diff --git a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenContent.kt b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenContent.kt index 459cca941e..50f06319ad 100644 --- a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenContent.kt +++ b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenContent.kt @@ -1,18 +1,25 @@ package mihon.feature.upcoming.anime import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material3.Badge import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.AppBar @@ -27,9 +34,9 @@ import mihon.feature.upcoming.components.calendar.Calendar import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn -import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import java.time.LocalDate import java.time.YearMonth @@ -99,6 +106,33 @@ private fun UpcomingToolbar() { ) } +@Composable +private fun DateHeading( + date: LocalDate, + animeCount: Int, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = relativeDateText(date), + modifier = Modifier + .padding(MaterialTheme.padding.small) + .padding(start = MaterialTheme.padding.small), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium, + ) + Badge( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) { + Text("$animeCount") + } + } +} + @Composable private fun UpcomingAnimeScreenSmallImpl( listState: LazyListState, @@ -140,7 +174,10 @@ private fun UpcomingAnimeScreenSmallImpl( ) } is UpcomingAnimeUIModel.Header -> { - ListGroupHeader(text = relativeDateText(item.date)) + DateHeading( + date = item.date, + animeCount = item.animeCount, + ) } } } @@ -188,7 +225,10 @@ private fun UpcomingAnimeScreenLargeImpl( ) } is UpcomingAnimeUIModel.Header -> { - ListGroupHeader(text = relativeDateText(item.date)) + DateHeading( + date = item.date, + animeCount = item.animeCount, + ) } } } diff --git a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenModel.kt b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenModel.kt index 952cf8ae15..b6ecc08e79 100644 --- a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeScreenModel.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMapIndexedNotNull import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.core.util.insertSeparators +import eu.kanade.core.util.insertSeparatorsReversed import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -33,7 +33,7 @@ class UpcomingAnimeScreenModel( val upcomingItems = it.toUpcomingAnimeUIModels() state.copy( items = upcomingItems, - events = it.toEvents(), + events = upcomingItems.toEvents(), headerIndexes = upcomingItems.getHeaderIndexes(), ) } @@ -42,13 +42,16 @@ class UpcomingAnimeScreenModel( } private fun List.toUpcomingAnimeUIModels(): ImmutableList { + var animeCount = 0 return fastMap { UpcomingAnimeUIModel.Item(it) } - .insertSeparators { before, after -> + .insertSeparatorsReversed { before, after -> + if (after != null) animeCount++ + val beforeDate = before?.anime?.expectedNextUpdate?.toLocalDate() val afterDate = after?.anime?.expectedNextUpdate?.toLocalDate() if (beforeDate != afterDate && afterDate != null) { - UpcomingAnimeUIModel.Header(afterDate) + UpcomingAnimeUIModel.Header(afterDate, animeCount).also { animeCount = 0 } } else { null } @@ -56,9 +59,9 @@ class UpcomingAnimeScreenModel( .toImmutableList() } - private fun List.toEvents(): ImmutableMap { - return groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } - .mapValues { it.value.size } + private fun List.toEvents(): ImmutableMap { + return filterIsInstance() + .associate { it.date to it.animeCount } .toImmutableMap() } diff --git a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeUIModel.kt b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeUIModel.kt index 11110c81cd..91ef3d66ab 100644 --- a/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeUIModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/anime/UpcomingAnimeUIModel.kt @@ -4,6 +4,6 @@ import tachiyomi.domain.entries.anime.model.Anime import java.time.LocalDate sealed interface UpcomingAnimeUIModel { - data class Header(val date: LocalDate) : UpcomingAnimeUIModel + data class Header(val date: LocalDate, val animeCount: Int) : UpcomingAnimeUIModel data class Item(val anime: Anime) : UpcomingAnimeUIModel } diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt index da511ddada..6b32c7f895 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/Calendar.kt @@ -32,7 +32,7 @@ import java.time.temporal.WeekFields import java.util.Locale private val FontSize = 16.sp -private const val DaysOfWeek = 7 +private const val DAYS_OF_WEEK = 7 @Composable fun Calendar( @@ -54,7 +54,7 @@ fun Calendar( modifier = Modifier .fillMaxWidth() .padding(vertical = MaterialTheme.padding.small) - .padding(start = MaterialTheme.padding.medium) + .padding(start = MaterialTheme.padding.medium), ) CalendarGrid( selectedYearMonth = selectedYearMonth, @@ -72,8 +72,8 @@ private fun CalendarGrid( ) { val localeFirstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value val weekDays = remember { - (0 until DaysOfWeek) - .map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DaysOfWeek + 1) } + (0 until DAYS_OF_WEEK) + .map { DayOfWeek.of((localeFirstDayOfWeek - 1 + it) % DAYS_OF_WEEK + 1) } .toImmutableList() } @@ -81,12 +81,12 @@ private fun CalendarGrid( val daysInMonth = selectedYearMonth.lengthOfMonth() VerticalGrid( - columns = SimpleGridCells.Fixed(DaysOfWeek), + columns = SimpleGridCells.Fixed(DAYS_OF_WEEK), modifier = if (isMediumWidthWindow() && !isExpandedWidthWindow()) { Modifier.widthIn(max = 360.dp) } else { Modifier - } + }, ) { weekDays.fastForEach { item -> Text( diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt index 46ed355abb..3a8f3e6fb0 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarDay.kt @@ -19,9 +19,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import java.time.LocalDate -private const val MaxEvents = 3 +private const val MAX_EVENTS = 3 @Composable fun CalendarDay( @@ -39,7 +40,7 @@ fun CalendarDay( Modifier.border( border = BorderStroke( width = 1.dp, - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ), shape = CircleShape, ) @@ -57,14 +58,14 @@ fun CalendarDay( textAlign = TextAlign.Center, fontSize = 16.sp, color = if (date.isBefore(today)) { - MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f) + MaterialTheme.colorScheme.onBackground.copy(alpha = DISABLED_ALPHA) } else { MaterialTheme.colorScheme.onBackground }, fontWeight = FontWeight.SemiBold, ) Row(Modifier.offset(y = 12.dp)) { - val size = events.coerceAtMost(MaxEvents) + val size = events.coerceAtMost(MAX_EVENTS) for (index in 0 until size) { CalendarIndicator( index = index, diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt index 55498ebb90..f5dbf5c485 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarHeader.kt @@ -63,20 +63,20 @@ fun CalenderHeader( } } -private const val MonthYearChangeAnimationDuration = 200 +private const val MONTH_YEAR_CHANGE_ANIMATION_DURATION = 200 private fun AnimatedContentTransitionScope.getAnimation(): ContentTransform { val movingForward = targetState > initialState val enterTransition = slideInVertically( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + animationSpec = tween(durationMillis = MONTH_YEAR_CHANGE_ANIMATION_DURATION), ) { height -> if (movingForward) height else -height } + fadeIn( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + animationSpec = tween(durationMillis = MONTH_YEAR_CHANGE_ANIMATION_DURATION), ) val exitTransition = slideOutVertically( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + animationSpec = tween(durationMillis = MONTH_YEAR_CHANGE_ANIMATION_DURATION), ) { height -> if (movingForward) -height else height } + fadeOut( - animationSpec = tween(durationMillis = MonthYearChangeAnimationDuration), + animationSpec = tween(durationMillis = MONTH_YEAR_CHANGE_ANIMATION_DURATION), ) return (enterTransition togetherWith exitTransition) .using(SizeTransform(clip = false)) diff --git a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt index 9aaca69de0..f9e223107c 100644 --- a/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt +++ b/app/src/main/java/mihon/feature/upcoming/components/calendar/CalendarIndicator.kt @@ -12,8 +12,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -private const val IndicatorScale = 12 -private const val IndicatorAlphaMultiplier = 0.3f +private const val INDICATOR_SCALE = 12 +private const val INDICATOR_ALPHA_MULTIPLIER = 0.3f @Composable fun CalendarIndicator( @@ -26,7 +26,7 @@ fun CalendarIndicator( modifier = modifier .padding(horizontal = 1.dp) .clip(shape = CircleShape) - .background(color = color.copy(alpha = (index + 1) * IndicatorAlphaMultiplier)) - .size(size = size.div(IndicatorScale)), + .background(color = color.copy(alpha = (index + 1) * INDICATOR_ALPHA_MULTIPLIER)) + .size(size = size.div(INDICATOR_SCALE)), ) } diff --git a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenContent.kt b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenContent.kt index 0ff267f019..526a8cea53 100644 --- a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenContent.kt +++ b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenContent.kt @@ -1,18 +1,25 @@ package mihon.feature.upcoming.manga import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material3.Badge import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.AppBar @@ -27,9 +34,9 @@ import mihon.feature.upcoming.manga.components.UpcomingItem import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.FastScrollLazyColumn -import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import java.time.LocalDate import java.time.YearMonth @@ -99,6 +106,33 @@ private fun UpcomingToolbar() { ) } +@Composable +private fun DateHeading( + date: LocalDate, + mangaCount: Int, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = relativeDateText(date), + modifier = Modifier + .padding(MaterialTheme.padding.small) + .padding(start = MaterialTheme.padding.small), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyMedium, + ) + Badge( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) { + Text("$mangaCount") + } + } +} + @Composable private fun UpcomingMangaScreenSmallImpl( listState: LazyListState, @@ -140,7 +174,10 @@ private fun UpcomingMangaScreenSmallImpl( ) } is UpcomingMangaUIModel.Header -> { - ListGroupHeader(text = relativeDateText(item.date)) + DateHeading( + date = item.date, + mangaCount = item.mangaCount, + ) } } } @@ -188,7 +225,10 @@ private fun UpcomingMangaScreenLargeImpl( ) } is UpcomingMangaUIModel.Header -> { - ListGroupHeader(text = relativeDateText(item.date)) + DateHeading( + date = item.date, + mangaCount = item.mangaCount, + ) } } } diff --git a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenModel.kt b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenModel.kt index 925f7207ee..07b3c6e67e 100644 --- a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaScreenModel.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMapIndexedNotNull import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import eu.kanade.core.util.insertSeparators +import eu.kanade.core.util.insertSeparatorsReversed import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -33,7 +33,7 @@ class UpcomingMangaScreenModel( val upcomingItems = it.toUpcomingMangaUIModels() state.copy( items = upcomingItems, - events = it.toEvents(), + events = upcomingItems.toEvents(), headerIndexes = upcomingItems.getHeaderIndexes(), ) } @@ -42,13 +42,16 @@ class UpcomingMangaScreenModel( } private fun List.toUpcomingMangaUIModels(): ImmutableList { + var mangaCount = 0 return fastMap { UpcomingMangaUIModel.Item(it) } - .insertSeparators { before, after -> + .insertSeparatorsReversed { before, after -> + if (after != null) mangaCount++ + val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate() val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate() if (beforeDate != afterDate && afterDate != null) { - UpcomingMangaUIModel.Header(afterDate) + UpcomingMangaUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 } } else { null } @@ -56,9 +59,9 @@ class UpcomingMangaScreenModel( .toImmutableList() } - private fun List.toEvents(): ImmutableMap { - return groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } - .mapValues { it.value.size } + private fun List.toEvents(): ImmutableMap { + return filterIsInstance() + .associate { it.date to it.mangaCount } .toImmutableMap() } diff --git a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaUIModel.kt b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaUIModel.kt index f8d83068d0..412eebb2d1 100644 --- a/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaUIModel.kt +++ b/app/src/main/java/mihon/feature/upcoming/manga/UpcomingMangaUIModel.kt @@ -4,6 +4,6 @@ import tachiyomi.domain.entries.manga.model.Manga import java.time.LocalDate sealed interface UpcomingMangaUIModel { - data class Header(val date: LocalDate) : UpcomingMangaUIModel + data class Header(val date: LocalDate, val mangaCount: Int) : UpcomingMangaUIModel data class Item(val manga: Manga) : UpcomingMangaUIModel } diff --git a/app/src/main/res/anim/player_enter_bottom.xml b/app/src/main/res/anim/player_enter_bottom.xml index 03e45f6fc5..d0869aa84b 100644 --- a/app/src/main/res/anim/player_enter_bottom.xml +++ b/app/src/main/res/anim/player_enter_bottom.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_enter_left.xml b/app/src/main/res/anim/player_enter_left.xml index af1035abae..dd695a2de7 100644 --- a/app/src/main/res/anim/player_enter_left.xml +++ b/app/src/main/res/anim/player_enter_left.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_enter_right.xml b/app/src/main/res/anim/player_enter_right.xml index f935394a29..16e5058cef 100644 --- a/app/src/main/res/anim/player_enter_right.xml +++ b/app/src/main/res/anim/player_enter_right.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_enter_top.xml b/app/src/main/res/anim/player_enter_top.xml index 3fd05d20cf..1e588ef0cd 100644 --- a/app/src/main/res/anim/player_enter_top.xml +++ b/app/src/main/res/anim/player_enter_top.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_exit_bottom.xml b/app/src/main/res/anim/player_exit_bottom.xml index 9dfe6064ff..f64a042afc 100644 --- a/app/src/main/res/anim/player_exit_bottom.xml +++ b/app/src/main/res/anim/player_exit_bottom.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_exit_left.xml b/app/src/main/res/anim/player_exit_left.xml index 77354c7937..5c095a9eea 100644 --- a/app/src/main/res/anim/player_exit_left.xml +++ b/app/src/main/res/anim/player_exit_left.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_exit_right.xml b/app/src/main/res/anim/player_exit_right.xml index f5304af2cf..909b5fe566 100644 --- a/app/src/main/res/anim/player_exit_right.xml +++ b/app/src/main/res/anim/player_exit_right.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_exit_top.xml b/app/src/main/res/anim/player_exit_top.xml index d0889cc200..641fae0628 100644 --- a/app/src/main/res/anim/player_exit_top.xml +++ b/app/src/main/res/anim/player_exit_top.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/anim/player_fade_in.xml b/app/src/main/res/anim/player_fade_in.xml index f1e93d0d16..fc204bf286 100644 --- a/app/src/main/res/anim/player_fade_in.xml +++ b/app/src/main/res/anim/player_fade_in.xml @@ -3,4 +3,4 @@ android:duration="@integer/player_animation_duration" android:fromAlpha="0.0" android:toAlpha="1.0" - android:interpolator="@android:interpolator/linear"/> \ No newline at end of file + android:interpolator="@android:interpolator/linear"/> diff --git a/app/src/main/res/anim/player_fade_out.xml b/app/src/main/res/anim/player_fade_out.xml index e2f8c89cd2..54d0b09c47 100644 --- a/app/src/main/res/anim/player_fade_out.xml +++ b/app/src/main/res/anim/player_fade_out.xml @@ -3,4 +3,4 @@ android:duration="@integer/player_animation_duration" android:fromAlpha="1.0" android:toAlpha="0.0" - android:interpolator="@android:interpolator/linear"/> \ No newline at end of file + android:interpolator="@android:interpolator/linear"/> diff --git a/app/src/main/res/drawable-hdpi/ic_book_open_variant_24dp.xml b/app/src/main/res/drawable-hdpi/ic_book_open_variant_24dp.xml index 071e043e1b..b371fd3894 100644 --- a/app/src/main/res/drawable-hdpi/ic_book_open_variant_24dp.xml +++ b/app/src/main/res/drawable-hdpi/ic_book_open_variant_24dp.xml @@ -6,4 +6,4 @@ android:viewportHeight="24" android:tint="@android:color/black"> - \ No newline at end of file + diff --git a/app/src/main/res/drawable-hdpi/ic_page_next_outline_24dp.xml b/app/src/main/res/drawable-hdpi/ic_page_next_outline_24dp.xml index 930be5ded7..8c8479dd9a 100644 --- a/app/src/main/res/drawable-hdpi/ic_page_next_outline_24dp.xml +++ b/app/src/main/res/drawable-hdpi/ic_page_next_outline_24dp.xml @@ -6,4 +6,4 @@ android:viewportWidth="24" android:viewportHeight="24"> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_ani.xml b/app/src/main/res/drawable/ic_ani.xml index dd239e28e8..db7ed9003a 100644 --- a/app/src/main/res/drawable/ic_ani.xml +++ b/app/src/main/res/drawable/ic_ani.xml @@ -14,4 +14,4 @@ android:fillColor="#000000" android:strokeColor="#000000" android:strokeLineCap="butt"/> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_ani_monochrome_launcher.xml b/app/src/main/res/drawable/ic_ani_monochrome_launcher.xml index 954c27544a..a2ce22b29d 100644 --- a/app/src/main/res/drawable/ic_ani_monochrome_launcher.xml +++ b/app/src/main/res/drawable/ic_ani_monochrome_launcher.xml @@ -13,4 +13,4 @@ android:pathData="m64,52.88l-15.13,-8.74c-0.9,-0.52 -2.03,0.13 -2.03,1.17v17.47c0,1.04 1.13,1.7 2.03,1.17L64,55.22c0.9,-0.52 0.9,-1.83 0,-2.35z" android:strokeWidth="0.0853242" android:fillColor="#000"/> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_animelibrary_filled_24dp.xml b/app/src/main/res/drawable/ic_animelibrary_filled_24dp.xml index 09e99bfde1..16184bfe50 100644 --- a/app/src/main/res/drawable/ic_animelibrary_filled_24dp.xml +++ b/app/src/main/res/drawable/ic_animelibrary_filled_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_animelibrary_selector_24dp.xml b/app/src/main/res/drawable/ic_animelibrary_selector_24dp.xml index 870e379981..fd41dc95f8 100644 --- a/app/src/main/res/drawable/ic_animelibrary_selector_24dp.xml +++ b/app/src/main/res/drawable/ic_animelibrary_selector_24dp.xml @@ -19,4 +19,4 @@ android:fromId="@id/checked" android:toId="@id/normal" /> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_banner_foreground.xml b/app/src/main/res/drawable/ic_banner_foreground.xml index ff90308af6..9b5317b651 100644 --- a/app/src/main/res/drawable/ic_banner_foreground.xml +++ b/app/src/main/res/drawable/ic_banner_foreground.xml @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_brightness_negative_20dp.xml b/app/src/main/res/drawable/ic_brightness_negative_20dp.xml index a8f167db3a..dfa1c9edbe 100644 --- a/app/src/main/res/drawable/ic_brightness_negative_20dp.xml +++ b/app/src/main/res/drawable/ic_brightness_negative_20dp.xml @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_brightness_positive_20dp.xml b/app/src/main/res/drawable/ic_brightness_positive_20dp.xml index 97bbdf42a6..5c4839f0f3 100644 --- a/app/src/main/res/drawable/ic_brightness_positive_20dp.xml +++ b/app/src/main/res/drawable/ic_brightness_positive_20dp.xml @@ -32,4 +32,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_browse_filled_24dp.xml b/app/src/main/res/drawable/ic_browse_filled_24dp.xml index 6405492499..320a6d0aef 100644 --- a/app/src/main/res/drawable/ic_browse_filled_24dp.xml +++ b/app/src/main/res/drawable/ic_browse_filled_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_circle_200dp.xml b/app/src/main/res/drawable/ic_circle_200dp.xml index 1a845275a3..f42d850a37 100644 --- a/app/src/main/res/drawable/ic_circle_200dp.xml +++ b/app/src/main/res/drawable/ic_circle_200dp.xml @@ -6,4 +6,4 @@ android:topLeftRadius="500dp" android:topRightRadius="0dp"/> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_circle_right_200dp.xml b/app/src/main/res/drawable/ic_circle_right_200dp.xml index 50ecf2c17b..0027ca8eee 100644 --- a/app/src/main/res/drawable/ic_circle_right_200dp.xml +++ b/app/src/main/res/drawable/ic_circle_right_200dp.xml @@ -6,4 +6,4 @@ android:topLeftRadius="0dp" android:topRightRadius="500dp"/> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_discord_24dp.xml b/app/src/main/res/drawable/ic_discord_24dp.xml index 55b90ba896..e61a231daf 100644 --- a/app/src/main/res/drawable/ic_discord_24dp.xml +++ b/app/src/main/res/drawable/ic_discord_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_glasses_24dp.xml b/app/src/main/res/drawable/ic_glasses_24dp.xml index 581a599141..572c2f06d8 100644 --- a/app/src/main/res/drawable/ic_glasses_24dp.xml +++ b/app/src/main/res/drawable/ic_glasses_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_pause_circle_filled_24.xml b/app/src/main/res/drawable/ic_pause_circle_filled_24.xml index db21593ebf..b81f101131 100644 --- a/app/src/main/res/drawable/ic_pause_circle_filled_24.xml +++ b/app/src/main/res/drawable/ic_pause_circle_filled_24.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_picture_in_picture_20dp.xml b/app/src/main/res/drawable/ic_picture_in_picture_20dp.xml index 16bbea6d86..7df3cd8f7e 100644 --- a/app/src/main/res/drawable/ic_picture_in_picture_20dp.xml +++ b/app/src/main/res/drawable/ic_picture_in_picture_20dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_play_circle_filled_24.xml b/app/src/main/res/drawable/ic_play_circle_filled_24.xml index f384b32541..1f02dc8cf0 100644 --- a/app/src/main/res/drawable/ic_play_circle_filled_24.xml +++ b/app/src/main/res/drawable/ic_play_circle_filled_24.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_play_seek_triangle.xml b/app/src/main/res/drawable/ic_play_seek_triangle.xml index 46d337e504..822c545bec 100644 --- a/app/src/main/res/drawable/ic_play_seek_triangle.xml +++ b/app/src/main/res/drawable/ic_play_seek_triangle.xml @@ -8,4 +8,4 @@ android:fillColor="#FFFFFF" android:pathData="M3,2 L22,12 L3,22 Z" /> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_progress_clock_24dp.xml b/app/src/main/res/drawable/ic_progress_clock_24dp.xml index a0f4b9cc0d..619d53a14a 100644 --- a/app/src/main/res/drawable/ic_progress_clock_24dp.xml +++ b/app/src/main/res/drawable/ic_progress_clock_24dp.xml @@ -7,4 +7,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_tag_24dp.xml b/app/src/main/res/drawable/ic_tag_24dp.xml index d23067f241..6cbb48b56c 100644 --- a/app/src/main/res/drawable/ic_tag_24dp.xml +++ b/app/src/main/res/drawable/ic_tag_24dp.xml @@ -10,4 +10,4 @@ android:fillColor="#000000" android:pathData="M9,1h6v1h-6zM6,3C4.9,3 4,3.9 4,5v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2h-3V2H9v1H6zM6,6h12v2H6zM6,10h12v2H6zM6,14h6v2H6z" /> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_ungroup_24dp.xml b/app/src/main/res/drawable/ic_ungroup_24dp.xml index 8ac8c7a229..1df68e98c2 100644 --- a/app/src/main/res/drawable/ic_ungroup_24dp.xml +++ b/app/src/main/res/drawable/ic_ungroup_24dp.xml @@ -5,4 +5,4 @@ android:viewportWidth="24" android:viewportHeight="24"> - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_updates_outline_24dp.xml b/app/src/main/res/drawable/ic_updates_outline_24dp.xml index 2a1937ad7d..53d5363997 100644 --- a/app/src/main/res/drawable/ic_updates_outline_24dp.xml +++ b/app/src/main/res/drawable/ic_updates_outline_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_volume_off_24dp.xml b/app/src/main/res/drawable/ic_volume_off_24dp.xml index 63386c8356..1fc225463c 100644 --- a/app/src/main/res/drawable/ic_volume_off_24dp.xml +++ b/app/src/main/res/drawable/ic_volume_off_24dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/ic_volume_on_20dp.xml b/app/src/main/res/drawable/ic_volume_on_20dp.xml index e46c70ddbd..7efdaf5df9 100644 --- a/app/src/main/res/drawable/ic_volume_on_20dp.xml +++ b/app/src/main/res/drawable/ic_volume_on_20dp.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/material_popup_background.xml b/app/src/main/res/drawable/material_popup_background.xml index 09987c4159..29bb971679 100644 --- a/app/src/main/res/drawable/material_popup_background.xml +++ b/app/src/main/res/drawable/material_popup_background.xml @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/player_bar.xml b/app/src/main/res/drawable/player_bar.xml index a5c89d9186..8676241fa6 100644 --- a/app/src/main/res/drawable/player_bar.xml +++ b/app/src/main/res/drawable/player_bar.xml @@ -22,4 +22,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/dialog_stub_textinput.xml b/app/src/main/res/layout/dialog_stub_textinput.xml index 0fa70a21da..057f9c7185 100644 --- a/app/src/main/res/layout/dialog_stub_textinput.xml +++ b/app/src/main/res/layout/dialog_stub_textinput.xml @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/discord_login_activity.xml b/app/src/main/res/layout/discord_login_activity.xml index 4f935e045a..f631ed30dc 100644 --- a/app/src/main/res/layout/discord_login_activity.xml +++ b/app/src/main/res/layout/discord_login_activity.xml @@ -13,4 +13,4 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/player_chapters_item.xml b/app/src/main/res/layout/player_chapters_item.xml index 0caad5e045..bd11e5624c 100644 --- a/app/src/main/res/layout/player_chapters_item.xml +++ b/app/src/main/res/layout/player_chapters_item.xml @@ -14,4 +14,4 @@ android:textColor="?attr/colorOnBackground" app:drawableEndCompat="@drawable/ic_blank_24dp" tools:text="1080p" - app:drawableTint="?attr/colorOnBackground" /> \ No newline at end of file + app:drawableTint="?attr/colorOnBackground" /> diff --git a/app/src/main/res/layout/player_controls.xml b/app/src/main/res/layout/player_controls.xml index 8c474dea24..653828a986 100644 --- a/app/src/main/res/layout/player_controls.xml +++ b/app/src/main/res/layout/player_controls.xml @@ -485,4 +485,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/player_double_tap_seek_view.xml b/app/src/main/res/layout/player_double_tap_seek_view.xml index e727211b84..014154bba4 100644 --- a/app/src/main/res/layout/player_double_tap_seek_view.xml +++ b/app/src/main/res/layout/player_double_tap_seek_view.xml @@ -49,4 +49,4 @@ android:textSize="12sp" tools:text="10 seconds" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/player_tracks_item.xml b/app/src/main/res/layout/player_tracks_item.xml index 99d1b06ab2..22860723ba 100644 --- a/app/src/main/res/layout/player_tracks_item.xml +++ b/app/src/main/res/layout/player_tracks_item.xml @@ -15,4 +15,4 @@ app:drawableEndCompat="@drawable/ic_blank_24dp" tools:drawableEndCompat="@drawable/ic_check_24dp" tools:text="1080p" - app:drawableTint="?attr/colorOnBackground" /> \ No newline at end of file + app:drawableTint="?attr/colorOnBackground" /> diff --git a/app/src/main/res/layout/pref_skip_intro_length.xml b/app/src/main/res/layout/pref_skip_intro_length.xml index f343e464df..37ae4e9d50 100644 --- a/app/src/main/res/layout/pref_skip_intro_length.xml +++ b/app/src/main/res/layout/pref_skip_intro_length.xml @@ -14,4 +14,4 @@ app:min="1" /> - \ No newline at end of file + diff --git a/app/src/main/res/menu/expanded_controller.xml b/app/src/main/res/menu/expanded_controller.xml index add39c6b89..534cc75b0b 100644 --- a/app/src/main/res/menu/expanded_controller.xml +++ b/app/src/main/res/menu/expanded_controller.xml @@ -9,4 +9,4 @@ app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml b/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml index e1b4ef7ae5..0f2d13ef6d 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_logo.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap/ic_banner.xml b/app/src/main/res/mipmap/ic_banner.xml index 0b04e75720..36a96eed88 100644 --- a/app/src/main/res/mipmap/ic_banner.xml +++ b/app/src/main/res/mipmap/ic_banner.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap/ic_launcher.xml b/app/src/main/res/mipmap/ic_launcher.xml index e1b4ef7ae5..0f2d13ef6d 100644 --- a/app/src/main/res/mipmap/ic_launcher.xml +++ b/app/src/main/res/mipmap/ic_launcher.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap/ic_launcher_round.xml b/app/src/main/res/mipmap/ic_launcher_round.xml new file mode 100644 index 0000000000..4f4ecc0d2e --- /dev/null +++ b/app/src/main/res/mipmap/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml index 7ac6a4c411..c6821b0d20 100644 --- a/app/src/main/res/raw/keep.xml +++ b/app/src/main/res/raw/keep.xml @@ -1,3 +1,3 @@ \ No newline at end of file + tools:keep="@layout/md_*" /> diff --git a/app/src/main/res/values-night/colors_cottoncandy.xml b/app/src/main/res/values-night/colors_cottoncandy.xml index e22affcda7..289f398945 100644 --- a/app/src/main/res/values-night/colors_cottoncandy.xml +++ b/app/src/main/res/values-night/colors_cottoncandy.xml @@ -24,4 +24,3 @@ #9A4058 @color/cottoncandy_primary - diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 24200c4077..c79fcf02ae 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -4,8 +4,13 @@ diff --git a/app/src/main/res/values/colors_cottoncandy.xml b/app/src/main/res/values/colors_cottoncandy.xml index fc504bb8f2..69fb11c171 100644 --- a/app/src/main/res/values/colors_cottoncandy.xml +++ b/app/src/main/res/values/colors_cottoncandy.xml @@ -23,6 +23,5 @@ #FAEEEF #352F30 #FFB1C1 - @color/cottoncandy_primary + @color/cottoncandy_primary - diff --git a/app/src/main/res/values/ic_banner_background.xml b/app/src/main/res/values/ic_banner_background.xml index 9cd39c7816..a0645c7844 100644 --- a/app/src/main/res/values/ic_banner_background.xml +++ b/app/src/main/res/values/ic_banner_background.xml @@ -1,4 +1,4 @@ #9D0F3B - \ No newline at end of file + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0e43847e14..664b0307bc 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -15,6 +15,7 @@ @color/tachiyomi_onPrimary @color/tachiyomi_primaryContainer @color/tachiyomi_onPrimaryContainer + @color/tachiyomi_inversePrimary @color/tachiyomi_secondary @color/tachiyomi_onSecondary @color/tachiyomi_secondaryContainer @@ -29,14 +30,20 @@ @color/tachiyomi_onSurface @color/tachiyomi_surfaceVariant @color/tachiyomi_onSurfaceVariant - @color/tachiyomi_outline - @color/tachiyomi_inverseOnSurface + @color/tachiyomi_surfaceTint @color/tachiyomi_inverseSurface - @color/tachiyomi_primaryInverse - @color/error - @color/onError - @color/errorContainer - @color/onErrorContainer + @color/tachiyomi_inverseOnSurface + @color/tachiyomi_error + @color/tachiyomi_onError + @color/tachiyomi_errorContainer + @color/tachiyomi_onErrorContainer + @color/tachiyomi_outline + @color/tachiyomi_outlineVariant + @color/tachiyomi_surfaceContainer + @color/tachiyomi_surfaceContainerHigh + @color/tachiyomi_surfaceContainerHighest + @color/tachiyomi_surfaceContainerLow + @color/tachiyomi_surfaceContainerLowest @color/divider_default @drawable/line_divider @@ -52,7 +59,7 @@ @bool/lightStatusBar @android:color/transparent - @color/surface_amoled + @color/amoled_surface @null false false @@ -122,6 +129,7 @@ @color/greenapple_onPrimary @color/greenapple_primaryContainer @color/greenapple_onPrimaryContainer + @color/greenapple_inversePrimary @color/greenapple_secondary @color/greenapple_onSecondary @color/greenapple_secondaryContainer @@ -136,10 +144,22 @@ @color/greenapple_onSurface @color/greenapple_surfaceVariant @color/greenapple_onSurfaceVariant - @color/greenapple_outline - @color/greenapple_inverseOnSurface @color/greenapple_inverseSurface - @color/greenapple_primaryInverse + @color/greenapple_inverseOnSurface + @color/greenapple_error + @color/greenapple_onError + @color/greenapple_errorContainer + @color/greenapple_onErrorContainer + @color/greenapple_outline + @color/greenapple_outlineVariant + @color/greenapple_scrim + @color/greenapple_surfaceBright + @color/greenapple_surfaceDim + @color/greenapple_surfaceContainer + @color/greenapple_surfaceContainerHigh + @color/greenapple_surfaceContainerHighest + @color/greenapple_surfaceContainerLow + @color/greenapple_surfaceContainerLowest @@ -149,6 +169,7 @@ @color/lavender_onPrimary @color/lavender_primaryContainer @color/lavender_onPrimaryContainer + @color/lavender_inversePrimary @color/lavender_secondary @color/lavender_onSecondary @color/lavender_secondaryContainer @@ -163,11 +184,22 @@ @color/lavender_onSurface @color/lavender_surfaceVariant @color/lavender_onSurfaceVariant - @color/lavender_outline - @color/lavender_inverseOnSurface @color/lavender_inverseSurface - @color/lavender_primaryInverse - @color/lavender_elevationOverlay + @color/lavender_inverseOnSurface + @color/lavender_error + @color/lavender_onError + @color/lavender_errorContainer + @color/lavender_onErrorContainer + @color/lavender_outline + @color/lavender_outlineVariant + @color/lavender_scrim + @color/lavender_surfaceBright + @color/lavender_surfaceDim + @color/lavender_surfaceContainer + @color/lavender_surfaceContainerHigh + @color/lavender_surfaceContainerHighest + @color/lavender_surfaceContainerLow + @color/lavender_surfaceContainerLowest @@ -177,6 +209,7 @@ @color/midnightdusk_onPrimary @color/midnightdusk_primaryContainer @color/midnightdusk_onPrimaryContainer + @color/midnightdusk_inversePrimary @color/midnightdusk_secondary @color/midnightdusk_onSecondary @color/midnightdusk_secondaryContainer @@ -191,11 +224,15 @@ @color/midnightdusk_onSurface @color/midnightdusk_surfaceVariant @color/midnightdusk_onSurfaceVariant - @color/midnightdusk_outline - @color/midnightdusk_inverseOnSurface + @color/midnightdusk_surfaceTint @color/midnightdusk_inverseSurface - @color/midnightdusk_primaryInverse - @color/midnightdusk_elevationOverlay + @color/midnightdusk_inverseOnSurface + @color/midnightdusk_outline + @color/midnightdusk_surfaceContainer + @color/midnightdusk_surfaceContainerHigh + @color/midnightdusk_surfaceContainerHighest + @color/midnightdusk_surfaceContainerLow + @color/midnightdusk_surfaceContainerLowest @@ -232,6 +269,7 @@ @color/strawberry_onPrimary @color/strawberry_primaryContainer @color/strawberry_onPrimaryContainer + @color/strawberry_inversePrimary @color/strawberry_secondary @color/strawberry_onSecondary @color/strawberry_secondaryContainer @@ -246,10 +284,22 @@ @color/strawberry_onSurface @color/strawberry_surfaceVariant @color/strawberry_onSurfaceVariant - @color/strawberry_outline - @color/strawberry_inverseOnSurface @color/strawberry_inverseSurface - @color/strawberry_primaryInverse + @color/strawberry_inverseOnSurface + @color/strawberry_error + @color/strawberry_onError + @color/strawberry_errorContainer + @color/strawberry_onErrorContainer + @color/strawberry_outline + @color/strawberry_outlineVariant + @color/strawberry_scrim + @color/strawberry_surfaceBright + @color/strawberry_surfaceDim + @color/strawberry_surfaceContainer + @color/strawberry_surfaceContainerHigh + @color/strawberry_surfaceContainerHighest + @color/strawberry_surfaceContainerLow + @color/strawberry_surfaceContainerLowest @@ -259,6 +309,7 @@ @color/tako_onPrimary @color/tako_primaryContainer @color/tako_onPrimaryContainer + @color/tako_inversePrimary @color/tako_secondary @color/tako_onSecondary @color/tako_secondaryContainer @@ -273,11 +324,15 @@ @color/tako_onSurface @color/tako_surfaceVariant @color/tako_onSurfaceVariant - @color/tako_outline - @color/tako_inverseOnSurface + @color/tako_surfaceTint @color/tako_inverseSurface - @color/tako_primaryInverse - @color/tako_elevationOverlay + @color/tako_inverseOnSurface + @color/tako_outline + @color/tako_surfaceContainer + @color/tako_surfaceContainerHigh + @color/tako_surfaceContainerHighest + @color/tako_surfaceContainerLow + @color/tako_surfaceContainerLowest @@ -287,6 +342,7 @@ @color/tealturquoise_onPrimary @color/tealturquoise_primaryContainer @color/tealturquoise_onPrimaryContainer + @color/tealturquoise_inversePrimary @color/tealturquoise_secondary @color/tealturquoise_onSecondary @color/tealturquoise_secondaryContainer @@ -301,13 +357,15 @@ @color/tealturquoise_onSurface @color/tealturquoise_surfaceVariant @color/tealturquoise_onSurfaceVariant - @color/tealturquoise_outline - @color/tealturquoise_inverseOnSurface + @color/tealturquoise_surfaceTint @color/tealturquoise_inverseSurface - @color/tealturquoise_primaryInverse - @color/tealturquoise_tertiary - @color/tealturquoise_onTertiary - @color/tealturquoise_elevationOverlay + @color/tealturquoise_inverseOnSurface + @color/tealturquoise_outline + @color/tealturquoise_surfaceContainer + @color/tealturquoise_surfaceContainerHigh + @color/tealturquoise_surfaceContainerHighest + @color/tealturquoise_surfaceContainerLow + @color/tealturquoise_surfaceContainerLowest @@ -317,6 +375,7 @@ @color/yinyang_onPrimary @color/yinyang_primaryContainer @color/yinyang_onPrimaryContainer + @color/yinyang_inversePrimary @color/yinyang_secondary @color/yinyang_onSecondary @color/yinyang_secondaryContainer @@ -331,10 +390,15 @@ @color/yinyang_onSurface @color/yinyang_surfaceVariant @color/yinyang_onSurfaceVariant - @color/yinyang_outline - @color/yinyang_inverseOnSurface + @color/yinyang_surfaceTint @color/yinyang_inverseSurface - @color/yinyang_primaryInverse + @color/yinyang_inverseOnSurface + @color/yinyang_outline + @color/yinyang_surfaceContainer + @color/yinyang_surfaceContainerHigh + @color/yinyang_surfaceContainerHighest + @color/yinyang_surfaceContainerLow + @color/yinyang_surfaceContainerLowest @@ -344,6 +408,7 @@ @color/yotsuba_onPrimary @color/yotsuba_primaryContainer @color/yotsuba_onPrimaryContainer + @color/yotsuba_inversePrimary @color/yotsuba_secondary @color/yotsuba_onSecondary @color/yotsuba_secondaryContainer @@ -358,10 +423,15 @@ @color/yotsuba_onSurface @color/yotsuba_surfaceVariant @color/yotsuba_onSurfaceVariant - @color/yotsuba_outline - @color/yotsuba_inverseOnSurface + @color/yotsuba_surfaceTint @color/yotsuba_inverseSurface - @color/yotsuba_primaryInverse + @color/yotsuba_inverseOnSurface + @color/yotsuba_outline + @color/yotsuba_surfaceContainer + @color/yotsuba_surfaceContainerHigh + @color/yotsuba_surfaceContainerHighest + @color/yotsuba_surfaceContainerLow + @color/yotsuba_surfaceContainerLowest @@ -479,6 +549,7 @@ @color/tidalwave_onPrimary @color/tidalwave_primaryContainer @color/tidalwave_onPrimaryContainer + @color/tidalwave_inversePrimary @color/tidalwave_secondary @color/tidalwave_onSecondary @color/tidalwave_secondaryContainer @@ -493,10 +564,15 @@ @color/tidalwave_onSurface @color/tidalwave_surfaceVariant @color/tidalwave_onSurfaceVariant - @color/tidalwave_outline - @color/tidalwave_inverseOnSurface + @color/tidalwave_surfaceTint @color/tidalwave_inverseSurface - @color/tidalwave_primaryInverse + @color/tidalwave_inverseOnSurface + @color/tidalwave_outline + @color/tidalwave_surfaceContainer + @color/tidalwave_surfaceContainerHigh + @color/tidalwave_surfaceContainerHighest + @color/tidalwave_surfaceContainerLow + @color/tidalwave_surfaceContainerLowest @@ -506,6 +582,7 @@ @color/nord_onPrimary @color/nord_primaryContainer @color/nord_onPrimaryContainer + @color/nord_inversePrimary @color/nord_secondary @color/nord_onSecondary @color/nord_secondaryContainer @@ -520,16 +597,21 @@ @color/nord_onSurface @color/nord_surfaceVariant @color/nord_onSurfaceVariant - @color/nord_outline - @color/nord_inverseOnSurface + @color/nord_surfaceTint @color/nord_inverseSurface - @color/nord_primaryInverse + @color/nord_inverseOnSurface @color/nord_onError @color/nord_errorContainer @color/nord_onErrorContainer - @color/nord_elevationOverlay + @color/nord_outline + @color/nord_outlineVariant + @color/nord_surfaceContainer + @color/nord_surfaceContainerHigh + @color/nord_surfaceContainerHighest + @color/nord_surfaceContainerLow + @color/nord_surfaceContainerLowest - +