Compare commits

...

110 Commits

Author SHA1 Message Date
Jobobby04 7ec14cd9f0 Fix preview 2024-08-11 20:42:40 -04:00
AntsyLich c23c9491fc Handle Android SDK 35 API collision
(cherry picked from commit fdb96179c6373eb0a8e7d6daea671a315d5ce5f0)
2024-08-11 19:41:15 -04:00
Jobobby04 29f3766c87 Update version code 2024-08-11 19:37:48 -04:00
Jobobby04 07c89890bc Fix SY migrations 2024-08-11 19:37:05 -04:00
Weblate (bot) 64a54f55b3 Translations update from Hosted Weblate (#1228)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Ahmed seif al-nasr <ahmdsyfalnsr2@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Howard Wu <HowardWu20@outlook.com>
Co-authored-by: TawfikSharaf <tawfikahmed132.wa@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2024-08-11 18:39:57 -04:00
Tim Schneeberger f7202e67cc feat(migration): add option to only show entries with new chapters (#1238) 2024-08-11 18:30:19 -04:00
renovate[bot] 155b03c176 fix(deps): update dependency com.elvishew:xlog to v1.11.1 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 18:29:49 -04:00
renovate[bot] 6b0482576b chore(deps): update gradle/actions action to v4 (#1243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-11 18:29:42 -04:00
AntsyLich c137bafd68 Fix UI freeze after migration
Fixes #938

(cherry picked from commit 3f1d28c3833e6b868152149ed02b3fb8c54eccef)
2024-08-11 18:09:02 -04:00
AntsyLich 49bdffdc28 Add a button to select all scanlators
Resolves #943
Closes #1109

(cherry picked from commit 84b2164787a795f3fd757c325cbfb6ef660ac3a3)
2024-08-11 18:08:43 -04:00
Catting f1b32d531a Add Copy Tracker URL on icon long press (#1101)
* 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>
(cherry picked from commit 200d39e023af79b02276554a1bef1d7d53e3b903)
2024-08-11 18:02:09 -04:00
Weblate (bot) a5ec6c5cdd 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 <ahmdsyfalnsr2@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: Animeboynz <40583749+Animeboynz@users.noreply.github.com>
Co-authored-by: David Katrinka <davidkatrinka1995@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Iker Lerones <ikerlero@hotmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Matyáš Caras <matyas@caras.wtf>
Co-authored-by: Norsze <norbert.szabo7+github@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gekka <1778962971@qq.com>
Co-authored-by: sebastians17 <sebastians117.ss@gmail.com>
Co-authored-by: vodkapmp <vodkapmp@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: Артём Голуб <artemtirax2001@gmail.com>
(cherry picked from commit b1b15a93eec15a82e2e83650abf97c1b9f0c501c)
2024-08-11 18:02:02 -04:00
MajorTanya 9c56cdb1c1 Fix MAL search results not showing start dates (#1098)
The previous approach would always throw an Exception because
`SimpleDateFormat.format()` expects the input to be of type `Date` or
`Number`, not `String`.

(cherry picked from commit 97c81fadb426d71ac99c9443ab0e89f4089046ef)
2024-08-11 18:01:51 -04:00
MajorTanya 543de065a6 Change Kitsu to kitsu.app domain (#1106)
cf. https://github.com/hummingbird-me/kitsu-server/commit/244fdccca9754d8579c049e738832843001b33b1

(cherry picked from commit 9240eceedc5e2b065dd680819c4180c1ae09512b)

# Conflicts:
#	README.md
2024-08-11 18:01:43 -04:00
Jobobby04 33296e1faf Translations readme 2024-08-11 18:00:59 -04:00
Catting d1a90c0bb7 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)
2024-08-11 18:00:34 -04:00
renovate[bot] 9fa61d33be 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)
2024-08-11 18:00:26 -04:00
renovate[bot] 0e9dcc7855 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)
2024-08-11 18:00:18 -04:00
renovate[bot] 6738c6072d 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)
2024-08-11 18:00:11 -04:00
renovate[bot] 29033c539c 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)
2024-08-11 18:00:04 -04:00
renovate[bot] eaa3413c37 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)
2024-08-11 17:59:57 -04:00
AntsyLich 73d9d1d46d ExpandableMangaDescription: Adjust size transform anim spec
Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 1c16fc79c2ac4c4be30308fed84ffb371dab5902)
2024-08-11 17:59:47 -04:00
Roshan Varughese 94f9aaf351 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>
(cherry picked from commit 31263084eca3ba98624d258a317d53094bba2256)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt
2024-08-11 17:59:35 -04:00
AntsyLich e21149cb37 Rename backup restore error log file
(cherry picked from commit 2858ef835fec8d7278b1d0cad1b5664104d1e4b0)
2024-08-11 17:39:41 -04:00
renovate[bot] 11aad16f59 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)
2024-08-11 17:39:30 -04:00
FooIbar 33a3918e86 Don't crash on ill-formed URLs (#1084)
(cherry picked from commit 854474f85ffc41eccdc2b3a6cf105fa2805ebc3c)
2024-08-11 17:39:21 -04:00
Tran M. Cuong d655b8ecdf fix: drawScrollbar crash on list with 0 item but only sticky header (#1083)
(cherry picked from commit 04db46fe75c2406fe9750e97da65774a6b268f27)
2024-08-11 17:39:12 -04:00
FooIbar 70a8bef7a5 Match extra layout space with scroll distance (#1076)
And increase recycler item view cache size.

(cherry picked from commit a3dfd2efe6ace7a2a4d79bd09fb1a729989f1094)
2024-08-11 17:38:58 -04:00
Vetle Ledaal 999a8613cf Improve error message if restoring from JSON file (#1056)
* Improve error message if restoring from JSON file

* Replace Exception with IOException

* Use more generic error message if protobuf fails

* fix lint

(cherry picked from commit de8ef6dad7c89afb7041ccb489d68539a4849cb5)
2024-08-11 17:38:48 -04:00
AntsyLich 5721a02bca Bump default user agent string
(cherry picked from commit 8160b47ff5fbbd9b32caeb462b5be881fabd3449)
2024-08-11 17:38:39 -04:00
AntsyLich e303b88b90 Cleanup backup/restore related code
(cherry picked from commit c201b341a716b90d378dcda4bd9b8ac4a343d4fc)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
2024-08-11 17:38:13 -04:00
AntsyLich a62dd5821a Fix library is backed up when disabled and make categories backup/restore independent
(cherry picked from commit 56fb4f62a152e87a71892aa68c78cac51a2c8596)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt
2024-08-11 17:34:04 -04:00
Roshan Varughese a0786d9b09 Adds Option to Copy Panel to Clipboard (#1003)
* 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>
(cherry picked from commit 0af90999c8eed4b6c56a94418e5558833f273aa9)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/reader/ReaderPageActionsDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2024-08-11 17:25:33 -04:00
renovate[bot] 04580ce357 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)
2024-08-11 15:39:47 -04:00
Roshan Varughese b759f2f02a Format Category String on Subtitle Display (#1030)
* 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>
(cherry picked from commit 88efde8796b0e1cc8fba6cd987bdc487bd97f248)
2024-08-11 15:39:38 -04:00
renovate[bot] 8ae8068ecd 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)
2024-08-11 15:39:31 -04:00
renovate[bot] eecd9367d4 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)
2024-08-11 15:39:24 -04:00
renovate[bot] 55dee69838 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)
2024-08-11 15:39:16 -04:00
FooIbar a730ca5444 Remove obsolete workaround (#1021)
(cherry picked from commit 51b68cd25ff4bf556de88cb31525c55dd7eb7027)
2024-08-11 15:39:06 -04:00
renovate[bot] cebd8fe0a8 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)
2024-08-11 15:38:54 -04:00
renovate[bot] 55a979c5f7 fix(deps): update dependency io.mockk:mockk to v1.13.12 (#1016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 4f61b2e4e89bc257cf5e629823904805907bf75c)
2024-08-11 15:38:37 -04:00
renovate[bot] 728f3fc349 chore(deps): update dependency gradle to v8.9 (#1007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f63e95091013320b27bfc3c7c975c4bdd4a983c5)
2024-08-11 15:38:17 -04:00
renovate[bot] a9a3ed1d16 fix(deps): update dependency org.jsoup:jsoup to v1.18.1 (#999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f3f2bd41c3974878bcf0e3a62d99ee89bf92fb41)
2024-08-11 15:38:08 -04:00
renovate[bot] 36f13a7c6a fix(deps): update dependency com.android.tools.build:gradle to v8.5.1 (#1010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 7a2ca4bf4d41764705637d61c6d86249f8815b7b)
2024-08-11 15:37:37 -04:00
renovate[bot] 37a2ccc678 Bump coil version and some cleanup
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit e65634cb427eafe9e3bd192f9e8bf71f2243ce6c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
2024-08-11 15:37:16 -04:00
FooIbar bb39088dd7 Fix some issues when reading/saving images (#993)
* Fix unsupported mime type error when saving images

Avoid using platform mime type map to get extensions as it may not have
all mime types we support.

* Fix jxl images downloading/reading

(cherry picked from commit daa47e049327c4d8b1fe4724ed1b84897d81fcf2)

# Conflicts:
#	core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt
2024-08-11 15:34:57 -04:00
AntsyLich c5546e1095 Fix login prompts despite being logged in to trackers in Manga screen
(cherry picked from commit cbcd8bd6682023f728568f2b44da26124618aed7)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2024-08-11 15:32:28 -04:00
AntsyLich 2d12c670db Observe tracker login state instead of fetching once (#987)
* Observe tracker login state instead of fetching once

* Review changes

(cherry picked from commit 2092c81bad59fd745a8514af320e534ecf40a5da)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2024-08-11 15:15:44 -04:00
AntsyLich 3db4bccebc Make global search "Has result" sticky
Closes #133

(cherry picked from commit 5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649)

# Conflicts:
#	app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt
2024-08-11 14:49:11 -04:00
Roshan Varughese 2f23ad6bfd Smart Update Dialog Tweak (#977)
* Smart Update Dialog Fix

* Build Fail Change 1

* Commit Suggested Change

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* Build Fail Change 2

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit ddba71df37359e6abbbcc96b18685435961710dc)
2024-08-11 14:48:33 -04:00
CrepeTF de1898a2c9 Correct tako variable colours (#976)
(cherry picked from commit 75b5d966018aa917f57adf37370088a51e4914b2)
2024-08-11 14:48:18 -04:00
renovate[bot] eb135ec22d fix(deps): update lifecycle.version to v2.8.3 (#972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 77db8873f6753cc9db8f67b39d53685563380cc6)
2024-08-11 14:48:10 -04:00
WerctFourth bf6c646dc7 Update image-decoder revision (#971)
(cherry picked from commit bff6183cf3ef400d8ddcdccf7180e4139816cc09)
2024-08-11 14:48:01 -04:00
renovate[bot] 9ce16d5e1c fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-alpha07 (#960)
* fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-alpha07

* Fix build

---------

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 c0f9de88e70ef1db97c521993462ae27550b5790)
2024-08-11 14:47:41 -04:00
renovate[bot] 619ff726c8 fix(deps): update aboutlib.version to v11.2.2 (#965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 80cdebcdf467ed00e530651aeed2b36cc63b8356)
2024-08-11 14:47:22 -04:00
renovate[bot] 730ceaaf49 fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.10.3 (#962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 9e2f97eeb8a0f1d1b353dc3e77fb64d69b568674)
2024-08-11 14:47:13 -04:00
CrepeTF 07b701cb3c Theme fixes (#963)
* Fix theme issue with download progress indicator

* Fix theme issue with download progress indicator + better contrast

(cherry picked from commit e132cc405f23e18dd8d73626730821eae9051149)
2024-08-11 14:47:02 -04:00
Caio Oliveira b64c6b78ea buildSrc: Fix strange warning in ci build (#952)
* buildSrc: Fix strange warning

´Project accessors enabled, but root project name not explicitly set for 'buildSrc'. Checking out the project in different folders will impact the generated code and implicitly the buildscript classpath, breaking caching.´

* Update settings.gradle.kts

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 2674b849746f20c051dab3fd6edfad1594e41b42)
2024-08-11 14:46:50 -04:00
renovate[bot] 521bce5c08 fix(deps): update dependency androidx.test.espresso:espresso-core to v3.6.1 (#958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f34702d4fcc10f24953b21e883fb454778bbae77)
2024-08-11 14:46:36 -04:00
renovate[bot] a719ed8c9e fix(deps): update dependency androidx.test.ext:junit-ktx to v1.2.1 (#959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 7823966ddf2289fee743feaa58b906ab9179a4ed)
2024-08-11 14:46:22 -04:00
renovate[bot] f6fc2d7e2f fix(deps): update dependency net.zetetic:sqlcipher-android to v4.6.0 (#1221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 09:55:53 -04:00
renovate[bot] 48d43c4f07 chore(deps): update actions/upload-artifact action to v4 (#1222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 09:55:42 -04:00
renovate[bot] f4fa86b2dc chore(deps): update softprops/action-gh-release action to v2 (#1223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 09:55:29 -04:00
renovate[bot] 37db0dc1f6 fix(deps): update dependency com.google.oauth-client:google-oauth-client to v1.36.0 (#1220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-26 20:55:33 -04:00
renovate[bot] 1ada03b07a chore(deps): update actions/setup-java action to v4 (#1217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-26 20:54:25 -04:00
renovate[bot] f4c1e7c2d5 chore(deps): update damianreeves/write-file-action action to v1.3 (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-26 20:54:11 -04:00
Jobobby04 6c5282c598 Another 2024-06-26 20:39:26 -04:00
Jobobby04 7899474a36 Fix build errors 2024-06-26 20:38:12 -04:00
Jobobby04 225b419bba Update wrapper validation 2024-06-26 20:23:59 -04:00
Jobobby04 fa64103a1c Actual baseline 2024-06-26 20:20:12 -04:00
Jobobby04 57e0e99f06 Preview branch makes preview 2024-06-26 20:10:45 -04:00
Jobobby04 efde7afa8e Update baseline 2024-06-26 20:09:09 -04:00
Jobobby04 f929a4bc26 Move some libs to sylibs 2024-06-26 20:03:24 -04:00
renovate[bot] d35141c1cc chore: Configure Renovate (#1215)
* Add renovate.json

* Only update specific files

* Add a glob

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
2024-06-26 19:57:10 -04:00
renovate[bot] 6988966019 fix(deps): update serialization.version to v1.7.1 (#951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit d8fe7d32ca6bacbb74e9e80173625a993621e4b2)
2024-06-26 19:32:52 -04:00
Maddie Witman f6d8ebbb0a Added configuration options to e-ink page flashes (#625)
* Recommit for e-ink pref changes

* Fixed state holder for flash interval

* Detekt

* Refactor suggested by Antsy

* inverted currentDisplayRefresh check for early exit

(cherry picked from commit 2f86f25d5b24c2054a604802dc65b8bc3a99c7c0)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt
#	app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt
2024-06-26 19:32:42 -04:00
renovate[bot] ae45df9fcf fix(deps): update dependency androidx.test.ext:junit-ktx to v1.2.0 (#948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 36e40c099772d2cb53d4ec87b2b00f97fe455c98)
2024-06-26 19:31:12 -04:00
renovate[bot] f332344681 fix(deps): update dependency androidx.test.espresso:espresso-core to v3.6.0 (#947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 40754659a9e146626add56c494a6dc9873691f14)
2024-06-26 19:31:03 -04:00
AntsyLich c6abb340ca Cleanup in CommonMangaItem.kt
Closes #19

Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com>
(cherry picked from commit e17f70f7226ea031fc1f962c9dfea3e404ba53ad)
2024-06-26 19:30:50 -04:00
Tran M. Cuong 99dbb16a7a Fix Migrator test and also add the test to build script (#896)
* Fix MigratorTest after update to Kotlin 2.0.0

* add main module's test to build script

(cherry picked from commit e57638a49c759d36d25b92f26633df5bdfb0d2b3)

# Conflicts:
#	.github/workflows/build_pull_request.yml
#	.github/workflows/build_push.yml
2024-06-26 19:30:39 -04:00
FooIbar f62e8933d7 Fix unexpected skips in strong skipping mode (#940)
(cherry picked from commit 0ce1cf22cdbb7d82df3db1a901253b4973ab027f)

# Conflicts:
#	source-api/build.gradle.kts
2024-06-26 19:30:00 -04:00
renovate[bot] ee3c2fd79c fix(deps): update dependency io.github.fornewid:material-motion-compose-core to v2.0.1 (#945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f6ec53cdde32a17bc394e55b697a3b59bfd76e58)
2024-06-26 19:28:53 -04:00
renovate[bot] 6b08b873a8 fix(deps): update dependency com.google.firebase:firebase-analytics to v22.0.2 (#936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit b37357f9097730edb1d72f1297461e580286856c)
2024-06-26 19:28:43 -04:00
renovate[bot] a3f2f49ab8 fix(deps): update moko to v0.24.1 (#933)
* fix(deps): update moko to v0.24.1

* Fix build

---------

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 f58a05e91828a69c01d49d629e5bfa9ec7ae3ffc)
2024-06-26 19:28:27 -04:00
Weblate (bot) 524f5cc6ab Translations update from Hosted Weblate (#904)
* Translated using Weblate (Malayalam)

Currently translated at 16.9% (136 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/

* Translated using Weblate (Swedish)

Currently translated at 99.1% (797 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/

* Translated using Weblate (Arabic)

Currently translated at 99.5% (800 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (18 of 18 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sv/

---------

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: Norsze <norbert.szabo7+github@gmail.com>
Co-authored-by: Duh051 <duhduh272@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
(cherry picked from commit cf02119da55c431d0fb4c42ecfec3681d466ae43)
2024-06-26 19:16:39 -04:00
FooIbar a35e084b9e Fix R8 version configuration not working (#916)
This reverts commit f3226fb278cab87422255e04e647c50095b61529.

(cherry picked from commit 4182ae89a036525c5575961a68371df249ce384f)

# Conflicts:
#	build.gradle.kts
2024-06-26 19:16:27 -04:00
FooIbar 78f7fba67b Update R8 to fix NoSuchMethodError crash (#914)
(cherry picked from commit f3226fb278cab87422255e04e647c50095b61529)

# Conflicts:
#	build.gradle.kts
2024-06-26 19:15:56 -04:00
renovate[bot] 69d1db3018 fix(deps): update dependency com.android.tools.build:gradle to v8.5.0 (#901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 2e78bceb30908aca8e585f91942849a6e4e7cb15)
2024-06-26 19:15:21 -04:00
Weblate (bot) d1b317e5c8 Translations update from Hosted Weblate (#878)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/

* Translated using Weblate (Croatian)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/

* Translated using Weblate (Malayalam)

Currently translated at 15.5% (125 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/

* Translated using Weblate (Malayalam)

Currently translated at 15.5% (125 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/

* Translated using Weblate (Malayalam)

Currently translated at 94.4% (17 of 18 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ml/

---------

Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: Animeboynz <roshanvarughese@hotmail.com>
(cherry picked from commit aa1714b2acf0e5b16558ea703220f60d4ecd23e9)
2024-06-26 19:15:06 -04:00
AntsyLich fff40e031f Fix issue with creating and restoring backup
Fixes #881

(cherry picked from commit f696f209c6b3efb3148e1d587af9e42c71d8dc6f)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
2024-06-26 19:14:54 -04:00
renovate[bot] 5be2ec51ba fix(deps): update dependency androidx.glance:glance-appwidget to v1.1.0 (#890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit af57e124f2113f78028771f1579a356884d7ead7)
2024-06-26 19:13:53 -04:00
renovate[bot] 1c2a7af13e fix(deps): update lifecycle.version to v2.8.2 (#889)
fix(deps): update dependency androidx.lifecycle:lifecycle-runtime-ktx to v2.8.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 8e8ee69bbacb2260d0ae52808c02684e567119b9)
2024-06-26 19:13:44 -04:00
renovate[bot] 182158acb0 fix(deps): update dependency com.android.tools.build:gradle to v8.4.2 (#883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit e9d69a83febccf8840dad03597e3ac2a6aa3f972)
2024-06-26 19:13:34 -04:00
AntsyLich 21f92bfb3a Fix chapter number parsing when number is after unwanted tag
Fixes #554

Co-authored-by: Naputt1 <94742489+Naputt1@users.noreply.github.com>
(cherry picked from commit 6a80305d6c572da6c08c0c69f5c25ff26ecf7383)
2024-06-26 19:13:24 -04:00
AntsyLich a5522ef732 Check category order before restoring from backup
Closes #632

Co-authored-by: Cologler <10906962+Cologler@users.noreply.github.com>
(cherry picked from commit 119bcbf8ed2415664922ea77fadf0da1165d1732)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt
2024-06-26 19:13:14 -04:00
Weblate (bot) 239793f7fd Translations update from Hosted Weblate (#611)
* Translated using Weblate (Malayalam)

Currently translated at 12.9% (104 of 803 strings)

Translated using Weblate (Malayalam)

Currently translated at 94.4% (17 of 18 strings)

Translated using Weblate (Malayalam)

Currently translated at 11.8% (95 of 803 strings)

Added translation using Weblate (Malayalam)

Added translation using Weblate (Malayalam)

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

* Translated using Weblate (Italian)

Currently translated at 99.6% (800 of 803 strings)

Co-authored-by: Federico Pierantoni <federico.pieranton@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translation: Mihon/Mihon

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: B4LiN7 <B4LiN7@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/
Translation: Mihon/Mihon

* Translated using Weblate (Javanese)

Currently translated at 38.7% (311 of 803 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.7% (793 of 803 strings)

Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
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/jv/
Translation: Mihon/Mihon

* Translated using Weblate (Greek)

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translation: Mihon/Mihon

* Translated using Weblate (Serbian)

Currently translated at 99.2% (797 of 803 strings)

Co-authored-by: Rikishaaa <jebote90@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sr/
Translation: Mihon/Mihon

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: Blackiezin <mcperenan134@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translation: Mihon/Mihon

* Translated using Weblate (French)

Currently translated at 100.0% (18 of 18 strings)

Translated using Weblate (French)

Currently translated at 99.0% (795 of 803 strings)

Co-authored-by: LaQuiche426 <loic.dossantos42630@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

* Translated using Weblate (Portuguese)

Currently translated at 99.8% (802 of 803 strings)

Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt/
Translation: Mihon/Mihon

* Translated using Weblate (Vietnamese)

Currently translated at 100.0% (18 of 18 strings)

Translated using Weblate (Vietnamese)

Currently translated at 96.8% (778 of 803 strings)

Co-authored-by: Karuto <nguyenthaison609@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

* Translated using Weblate (Croatian)

Currently translated at 99.5% (799 of 803 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translation: Mihon/Mihon

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: Eji-san <ejierubani@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translation: Mihon/Mihon

* Translated using Weblate (Galician)

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: kevans <albapazpi@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/gl/
Translation: Mihon/Mihon

* Translated using Weblate (Ukrainian)

Currently translated at 99.8% (802 of 803 strings)

Co-authored-by: Kodekiro Kodekihara <lolbitoklol@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uk/
Translation: Mihon/Mihon

* Translated using Weblate (Malay)

Currently translated at 98.6% (792 of 803 strings)

Co-authored-by: Farith <mail2@farithadnan.net>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
Translation: Mihon/Mihon

* Translated using Weblate (Nepali)

Currently translated at 100.0% (18 of 18 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: FateXBlood <fatexblood@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

* Translated using Weblate (Vietnamese)

Currently translated at 100.0% (803 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/

* Translated using Weblate (Croatian)

Currently translated at 100.0% (803 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (18 of 18 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/es/

* Translated using Weblate (Romanian)

Currently translated at 99.6% (800 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (18 of 18 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ro/

* Translated using Weblate (Italian)

Currently translated at 100.0% (803 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/

* Translated using Weblate (Polish)

Currently translated at 99.5% (799 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (803 of 803 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (German)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/

* Translated using Weblate (Russian)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/

* Translated using Weblate (French)

Currently translated at 99.5% (800 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/

* Translated using Weblate (Filipino)

Currently translated at 99.8% (803 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/

* Translated using Weblate (Nepali)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/

* Translated using Weblate (Catalan)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (804 of 804 strings)

Translation: Mihon/Mihon
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/

* Translated using Weblate (Catalan)

Currently translated at 100.0% (18 of 18 strings)

Translation: Mihon/Mihon Plurals
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ca/

---------

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: Federico Pierantoni <federico.pieranton@gmail.com>
Co-authored-by: B4LiN7 <B4LiN7@users.noreply.hosted.weblate.org>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rikishaaa <jebote90@gmail.com>
Co-authored-by: Blackiezin <mcperenan134@gmail.com>
Co-authored-by: LaQuiche426 <loic.dossantos42630@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Karuto <nguyenthaison609@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: kevans <albapazpi@gmail.com>
Co-authored-by: Kodekiro Kodekihara <lolbitoklol@gmail.com>
Co-authored-by: Farith <mail2@farithadnan.net>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Chrono Lux <amber_c001@protonmail.com>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: sebastians17 <sebastians117.ss@gmail.com>
Co-authored-by: Tim Schneeberger <thebone.main@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Naga <yz2000.pro@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
(cherry picked from commit 87fe64468ca08466af5b9fcc7f9e17e9a23021e6)

# Conflicts:
#	i18n/src/commonMain/resources/MR/gl/strings.xml
#	i18n/src/commonMain/resources/MR/ro/strings.xml
2024-06-26 19:12:15 -04:00
renovate[bot] 4e9cfe4602 fix(deps): update dependency io.github.fornewid:material-motion-compose-core to v2 (#873)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit bdce3c39f1475dc77dad300a0bf3702e85d32916)
2024-06-26 19:11:01 -04:00
AntsyLich f548c85e7a MangaChapterListItem: Don't use alpha modifier
Possibly fixes #822

Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 15d999229fcce865001d5fa77d0163e6e80e38db)
2024-06-26 19:10:39 -04:00
renovate[bot] 576349c446 fix(deps): update okhttp monorepo to v5.0.0-alpha.14 (#688)
* fix(deps): update okhttp monorepo to v5.0.0-alpha.14

* Fix build

---------

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 1edd55c981aa72faf49c06173f33bf0c2f99fe60)
2024-06-26 19:10:28 -04:00
renovate[bot] 9b00e0458b fix(deps): update serialization.version to v1.7.0 (#870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 71b558cb34c4e2da435877f391e57b6d49c4ef4f)
2024-06-26 19:06:15 -04:00
renovate[bot] 6a1ff99441 chore(deps): update kotlin and compose compiler to v2 (major) (#819)
* chore(deps): update kotlin and compose compiler to v2

* Update .gitignore

* Fix build

---------

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 46003ec25139319079abc9fde89b3afd344a1a11)

# Conflicts:
#	.github/renovate.json5
#	gradle/compose.versions.toml
#	source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
2024-06-26 19:06:10 -04:00
renovate[bot] 0121fe9397 fix(deps): update dependency io.kotest:kotest-assertions-core to v5.9.1 (#869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1f7574bd4fc0471b7f974cffdf166c2551b2749b)
2024-06-26 17:41:20 -04:00
Cuong M. Tran 5c47c7a409 Fix MigratorTest after update to io.mockk v1.13.11 (#814)
* Fix MigratorTest after update to io.mockk v1.13.11

Causing error: io.mockk.MockKException: was not can only be called on a mocked object

* remove import

(cherry picked from commit da62c7a21a81f513988fa64df6253376f85228ef)
2024-06-26 17:40:33 -04:00
renovate[bot] 8bb4f33f2e fix(deps): update dependency io.github.fornewid:material-motion-compose-core to v1.2.1 (#858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 0870cffba121c0cc7db9c79f83f770a43d9c32e7)
2024-06-26 17:40:17 -04:00
Sven 5f5fd51668 fix: storage permission request for non-conforming devices (#726)
* fix: storage permission request for non-conforming devices

* fix: catch more specific exception

* chore: add toast message to indicate missing persistent permissions

* chore: correct newly introduced translaction string

* Change error toast message

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8632ba85ee1ed080d7baa70050d460807c8edfcf)
2024-06-26 17:40:03 -04:00
Jobobby04 c7bbad93b2 Fix some MDLang issues 2024-06-26 17:38:53 -04:00
Jobobby04 1a4a2506f4 Codestral(ChatGPT) cleanup of some double pages code 2024-06-26 17:38:51 -04:00
gelionexists 7b7a594ddb Update LewdMangaChecker.kt (#1204)
- Added the `mature` tag
- Added `doujins` (doujins.com) and `luscious` (luscious.net) as filter keywords
2024-06-26 17:31:41 -04:00
KaiserBh c2eece0fff chore: improve google drive sync. (#1200)
improve google drive sync, removes the lock, change to protobuf, and potentially fix deviceId not being unique, since it wasn't appState...
2024-06-26 17:31:12 -04:00
ɴᴇᴋᴏ d29a4ff381 Update TW strings.xml (#1202)
Add Google sync strings
2024-06-26 17:30:06 -04:00
305 changed files with 3047 additions and 1208 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"labels": ["Dependencies"],
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
}
+4 -4
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v4
build:
name: Build app
@@ -27,19 +27,19 @@ jobs:
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: adopt
- name: Set up gradle
uses: gradle/actions/setup-gradle@v3
uses: gradle/actions/setup-gradle@v4
- name: Build app
run: ./gradlew detekt assembleDevDebug
- name: Upload APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: TachiyomiSY-${{ github.sha }}.apk
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
+6 -6
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v2
uses: gradle/actions/wrapper-validation@v4
- name: Setup Android SDK
run: |
@@ -31,18 +31,18 @@ jobs:
distribution: adopt
- name: Set up gradle
uses: gradle/actions/setup-gradle@v3
uses: gradle/actions/setup-gradle@v4
# SY <--
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.2
uses: DamianReeves/write-file-action@v1.3
with:
path: app/google-services.json
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
write-mode: overwrite
- name: Write client_secrets.json
uses: DamianReeves/write-file-action@v1.2
uses: DamianReeves/write-file-action@v1.3
with:
path: app/src/main/assets/client_secrets.json
contents: ${{ secrets.CLIENT_SECRETS_TEXT }}
@@ -86,7 +86,7 @@ jobs:
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.run_number }}
name: TachiyomiSY
+2 -3
View File
@@ -3,12 +3,11 @@ name: Remote Dispatch Action Initiator
on:
push:
branches:
- 'master'
- 'preview'
jobs:
trigger_preview_build:
name: Trigger preview build
if: ${{ github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest
steps:
@@ -16,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v4
- name: Create Tag
run: |
+1
View File
@@ -1,4 +1,5 @@
.gradle
.kotlin
/local.properties
/.idea/workspace.xml
.DS_Store
+1 -1
View File
@@ -26,7 +26,7 @@ Before you start, please note that the ability to use following technologies is
## Linting
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
Run the `detekt` gradle task. If the build fails, a report of issues can be found in `app/build/reports/detekt/`. The report is availble in several formats and details each issue that needs attention.
## Getting help
+10 -1
View File
@@ -14,7 +14,7 @@ Features of Mihon(original) include:
* Online reading from a variety of sources
* Local reading of downloaded content
* A configurable reader with multiple viewers, reading directions and other 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/) support
* 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/) support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
@@ -67,6 +67,15 @@ Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/re
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
## Translation
Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/mihon/tachiyomisy/)
<details><summary>Translation Progress</summary>
<a href="https://hosted.weblate.org/engage/mihon/">
<img src="https://hosted.weblate.org/widgets/mihon/-/tachiyomisy/multi-auto.svg" alt="Translation status" />
</a>
</details>
## Issues, Feature Requests and Contributing
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
+9 -6
View File
@@ -29,7 +29,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 68
versionCode = 69
versionName = "1.10.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@@ -162,13 +162,15 @@ dependencies {
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.interpolator)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(libs.bundles.sqlite)
// SY -->
implementation(libs.sqlcipher)
implementation(sylibs.sqlcipher)
// SY <--
implementation(kotlinx.reflect)
@@ -246,9 +248,6 @@ dependencies {
implementation(libs.compose.grid)
implementation(libs.google.api.services.drive)
implementation(libs.google.api.client.oauth)
// Logging
implementation(libs.logcat)
@@ -281,6 +280,10 @@ dependencies {
// RatingBar (SY)
implementation(sylibs.ratingbar)
implementation(sylibs.composeRatingbar)
// Google drive
implementation(sylibs.google.api.services.drive)
implementation(sylibs.google.api.client.oauth)
}
androidComponents {
@@ -302,7 +305,7 @@ androidComponents {
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
@@ -49,6 +49,11 @@ class SourcePreferences(
emptySet(),
)
fun globalSearchFilterState() = preferenceStore.getBoolean(
Preference.appStateKey("has_filters_toggle_state"),
false,
)
// SY -->
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)
@@ -29,7 +29,7 @@ class SyncPreferences(
)
fun uniqueDeviceID(): String {
val uniqueIDPreference = preferenceStore.getString("unique_device_id", "")
val uniqueIDPreference = preferenceStore.getString(Preference.appStateKey("unique_device_id"), "")
// Retrieve the current value of the preference
var uniqueID = uniqueIDPreference.get()
@@ -53,12 +53,14 @@ class SyncPreferences(
tracking = preferenceStore.getBoolean("tracking", true).get(),
history = preferenceStore.getBoolean("history", true).get(),
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
// SY -->
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
readEntries = preferenceStore.getBoolean("readEntries", true).get()
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
savedSearches = preferenceStore.getBoolean("savedSearches", true).get(),
// SY <--
)
}
@@ -70,12 +72,14 @@ class SyncPreferences(
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
preferenceStore.getBoolean("history", true).set(syncSettings.history)
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
// SY -->
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
preferenceStore.getBoolean("savedSearches", true).set(syncSettings.savedSearches)
// SY <--
}
@@ -7,11 +7,13 @@ data class SyncSettings(
val tracking: Boolean = true,
val history: Boolean = true,
val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
// SY -->
val customInfo: Boolean = true,
val readEntries: Boolean = true
val readEntries: Boolean = true,
val savedSearches: Boolean = true,
// SY <--
)
@@ -155,7 +155,7 @@ private fun ColumnScope.FilterPage(
)
// SY <--
val trackers = remember { screenModel.trackers }
val trackers by screenModel.trackersFlow.collectAsState()
when (trackers.size) {
0 -> {
// No trackers
@@ -188,6 +188,7 @@ private fun ColumnScope.SortPage(
category: Category?,
screenModel: LibrarySettingsScreenModel,
) {
val trackers by screenModel.trackersFlow.collectAsState()
// SY -->
val globalSortMode by screenModel.libraryPreferences.sortingMode().collectAsState()
val sortingMode = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
@@ -206,12 +207,12 @@ private fun ColumnScope.SortPage(
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
// SY <--
val trackerSortOption =
if (screenModel.trackers.isEmpty()) {
emptyList()
} else {
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
}
val trackerSortOption = if (trackers.isEmpty()) {
emptyList()
} else {
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)
}
listOfNotNull(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
@@ -346,12 +347,13 @@ private fun ColumnScope.GroupPage(
screenModel: LibrarySettingsScreenModel,
hasCategories: Boolean,
) {
val groups = remember(hasCategories, screenModel.trackers) {
val trackers by screenModel.trackersFlow.collectAsState()
val groups = remember(hasCategories, trackers) {
buildList {
add(LibraryGroup.BY_DEFAULT)
add(LibraryGroup.BY_SOURCE)
add(LibraryGroup.BY_STATUS)
if (screenModel.trackers.isNotEmpty()) {
if (trackers.isNotEmpty()) {
add(LibraryGroup.BY_TRACK_STATUS)
}
if (hasCategories) {
@@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.manga.components.MangaCover
@@ -42,15 +43,22 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground
import tachiyomi.domain.manga.model.MangaCover as MangaCoverModel
object CommonMangaItemDefaults {
val GridHorizontalSpacer = 4.dp
val GridVerticalSpacer = 4.dp
@Suppress("ConstPropertyName")
const val BrowseFavoriteCoverAlpha = 0.34f
}
private val ContinueReadingButtonSize = 28.dp
private val ContinueReadingButtonSizeSmall = 28.dp
private val ContinueReadingButtonSizeLarge = 32.dp
private val ContinueReadingButtonIconSizeSmall = 16.dp
private val ContinueReadingButtonIconSizeLarge = 20.dp
private val ContinueReadingButtonGridPadding = 6.dp
private val ContinueReadingButtonListSpacing = 8.dp
@@ -62,7 +70,7 @@ private const val GridSelectedCoverAlpha = 0.76f
*/
@Composable
fun MangaCompactGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
coverData: MangaCoverModel,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false,
@@ -96,10 +104,12 @@ fun MangaCompactGridItem(
)
} else if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeLarge,
iconSize = ContinueReadingButtonIconSizeLarge,
onClick = onClickContinueReading,
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
}
},
@@ -148,11 +158,13 @@ private fun BoxScope.CoverTextOverlay(
)
if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeSmall,
iconSize = ContinueReadingButtonIconSizeSmall,
onClick = onClickContinueReading,
modifier = Modifier.padding(
end = ContinueReadingButtonGridPadding,
bottom = ContinueReadingButtonGridPadding,
),
onClickContinueReading = onClickContinueReading,
)
}
}
@@ -163,7 +175,7 @@ private fun BoxScope.CoverTextOverlay(
*/
@Composable
fun MangaComfortableGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
coverData: MangaCoverModel,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
@@ -194,10 +206,12 @@ fun MangaComfortableGridItem(
content = {
if (onClickContinueReading != null) {
ContinueReadingButton(
size = ContinueReadingButtonSizeLarge,
iconSize = ContinueReadingButtonIconSizeLarge,
onClick = onClickContinueReading,
modifier = Modifier
.padding(ContinueReadingButtonGridPadding)
.align(Alignment.BottomEnd),
onClickContinueReading = onClickContinueReading,
)
}
},
@@ -309,14 +323,14 @@ private fun GridItemSelectable(
private fun Modifier.selectedOutline(
isSelected: Boolean,
color: Color,
) = this then drawBehind { if (isSelected) drawRect(color = color) }
) = drawBehind { if (isSelected) drawRect(color = color) }
/**
* Layout of list item.
*/
@Composable
fun MangaListItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
coverData: MangaCoverModel,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
@@ -354,8 +368,10 @@ fun MangaListItem(
BadgeGroup(content = badge)
if (onClickContinueReading != null) {
ContinueReadingButton(
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
onClickContinueReading = onClickContinueReading,
size = ContinueReadingButtonSizeSmall,
iconSize = ContinueReadingButtonIconSizeSmall,
onClick = onClickContinueReading,
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing)
)
}
}
@@ -363,23 +379,25 @@ fun MangaListItem(
@Composable
private fun ContinueReadingButton(
size: Dp,
iconSize: Dp,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClickContinueReading: () -> Unit,
) {
Box(modifier = modifier) {
FilledIconButton(
onClick = onClickContinueReading,
modifier = Modifier.size(ContinueReadingButtonSize),
onClick = onClick,
shape = MaterialTheme.shapes.small,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
),
modifier = Modifier.size(size)
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(MR.strings.action_resume),
modifier = Modifier.size(16.dp),
modifier = Modifier.size(iconSize),
)
}
}
@@ -30,7 +30,6 @@ 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.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
@@ -69,9 +68,6 @@ fun MangaChapterListItem(
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
modifier: Modifier = Modifier,
) {
val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
val start = getSwipeAction(
action = chapterSwipeStartAction,
read = read,
@@ -136,15 +132,20 @@ fun MangaChapterListItem(
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f),
)
}
Row(modifier = Modifier.alpha(textSubtitleAlpha)) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
Row {
val subtitleStyle = MaterialTheme.typography.bodySmall
.merge(
color = LocalContentColor.current
.copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha)
)
ProvideTextStyle(value = subtitleStyle) {
if (date != null) {
Text(
text = date,
@@ -38,6 +38,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.view.updatePadding
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
@@ -102,9 +102,12 @@ fun SetIntervalDialog(
),
),
)
Spacer(Modifier.height(MaterialTheme.padding.small))
} else {
Text(
stringResource(MR.strings.manga_interval_expected_update_null),
)
}
Spacer(Modifier.height(MaterialTheme.padding.small))
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount))
@@ -2,6 +2,7 @@ package eu.kanade.presentation.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
@@ -289,7 +290,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("") }
@@ -111,8 +111,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) {
@@ -7,12 +7,12 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
@@ -23,8 +23,6 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp }
@@ -156,16 +154,14 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.TrackerPreference -> {
val uName by Injekt.get<TrackPreferences>()
.trackUsername(item.tracker)
.collectAsState()
item.tracker.run {
TrackingPreferenceWidget(
tracker = this,
checked = uName.isNotEmpty(),
onClick = { if (isLoggedIn) item.logout() else item.login() },
)
val isLoggedIn by item.tracker.let { tracker ->
tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn)
}
TrackingPreferenceWidget(
tracker = item.tracker,
checked = isLoggedIn,
onClick = { if (isLoggedIn) item.logout() else item.login() },
)
}
is Preference.PreferenceItem.InfoPreference -> {
InfoWidget(text = item.title)
@@ -127,7 +127,17 @@ object SettingsDataScreen : SearchableSettings {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
// For some reason InkBook devices do not implement the SAF properly. Persistable URI grants do not
// work. However, simply retrieving the URI and using it works fine for these devices. Access is not
// revoked after the app is closed or the device is restarted.
// This also holds for some Samsung devices. Thus, we simply execute inside of a try-catch block and
// ignore the exception if it is thrown.
try {
context.contentResolver.takePersistableUriPermission(uri, flags)
} catch (e: SecurityException) {
logcat(LogPriority.ERROR, e)
context.toast(MR.strings.file_picker_uri_permission_unsupported)
}
UniFile.fromUri(context, uri)?.let {
storageDirPref.set(it.uri.toString())
@@ -84,9 +84,6 @@ object SettingsLibraryScreen : SearchableSettings {
val scope = rememberCoroutineScope()
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
val defaultCategory by libraryPreferences.defaultCategory().collectAsState()
val selectedCategory = allCategories.find { it.id == defaultCategory.toLong() }
// For default category
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
allCategories.fastMap { it.id.toInt() }
@@ -108,7 +105,6 @@ object SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultCategory(),
title = stringResource(MR.strings.default_category),
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
entries = ids.zip(labels).toMap().toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
@@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
@@ -88,12 +89,8 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_page_transitions),
),
SY <-- */
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ),
),
getDisplayGroup(readerPreferences = readerPref),
getEInkGroup(readerPreferences = readerPref),
getReadingGroup(readerPreferences = readerPref),
getPagedGroup(readerPreferences = readerPref),
getWebtoonGroup(readerPreferences = readerPref),
@@ -156,6 +153,65 @@ object SettingsReaderScreen : SearchableSettings {
)
}
@Composable
private fun getEInkGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
val flashPageState by readerPreferences.flashOnPageChange().collectAsState()
val flashMillisPref = readerPreferences.flashDurationMillis()
val flashMillis by flashMillisPref.collectAsState()
val flashIntervalPref = readerPreferences.flashPageInterval()
val flashInterval by flashIntervalPref.collectAsState()
val flashColorPref = readerPreferences.flashColor()
return Preference.PreferenceGroup(
title = "E-Ink",
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ),
),
Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
min = 1,
max = 15,
title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.SliderPreference(
value = flashInterval,
min = 1,
max = 10,
title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onValueChanged = {
flashIntervalPref.set(it)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.ListPreference(
pref = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black),
),
enabled = flashPageState,
),
),
)
}
@Composable
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
@@ -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 {
@@ -104,7 +103,7 @@ class CreateBackupScreen : Screen() {
}
@Composable
private fun ColumnScope.Options(
private fun Options(
options: ImmutableList<BackupOptions.Entry>,
state: CreateBackupScreenModel.State,
model: CreateBackupScreenModel,
@@ -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()
@@ -49,7 +49,7 @@ class SyncSettingsSelector : Screen() {
LazyColumnWithAction(
contentPadding = contentPadding,
actionLabel = stringResource(SYMR.strings.label_sync),
actionEnabled = state.options.anyEnabled(),
actionEnabled = state.options.canCreate(),
onClickAction = {
if (!SyncDataJob.isRunning(context)) {
model.syncNow(context)
@@ -122,12 +122,14 @@ private class SyncSettingsSelectorModel(
tracking = syncSettings.tracking,
history = syncSettings.history,
appSettings = syncSettings.appSettings,
extensionRepoSettings = syncSettings.extensionRepoSettings,
sourceSettings = syncSettings.sourceSettings,
privateSettings = syncSettings.privateSettings,
// SY -->
customInfo = syncSettings.customInfo,
readEntries = syncSettings.readEntries,
savedSearches = syncSettings.savedSearches,
// SY <--
)
}
@@ -140,12 +142,14 @@ private class SyncSettingsSelectorModel(
tracking = backupOptions.tracking,
history = backupOptions.history,
appSettings = backupOptions.appSettings,
extensionRepoSettings = backupOptions.extensionRepoSettings,
sourceSettings = backupOptions.sourceSettings,
privateSettings = backupOptions.privateSettings,
// SY -->
customInfo = backupOptions.customInfo,
readEntries = backupOptions.readEntries,
savedSearches = backupOptions.savedSearches,
// SY <--
)
}
@@ -7,19 +7,42 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.milliseconds
@Stable
class DisplayRefreshHost {
internal var currentDisplayRefresh by mutableStateOf(false)
private val readerPreferences = Injekt.get<ReaderPreferences>()
internal val flashMillis = readerPreferences.flashDurationMillis()
internal val flashMode = readerPreferences.flashColor()
internal val flashIntervalPref = readerPreferences.flashPageInterval()
// Internal State for Flash
private var flashInterval = flashIntervalPref.get()
private var timesCalled = 0
fun flash() {
currentDisplayRefresh = true
if (timesCalled % flashInterval == 0) {
currentDisplayRefresh = true
}
timesCalled += 1
}
fun setInterval(interval: Int) {
flashInterval = interval
timesCalled = 0
}
}
@@ -29,18 +52,39 @@ fun DisplayRefreshHost(
modifier: Modifier = Modifier,
) {
val currentDisplayRefresh = hostState.currentDisplayRefresh
val refreshDuration by hostState.flashMillis.collectAsState()
val flashMode by hostState.flashMode.collectAsState()
val flashInterval by hostState.flashIntervalPref.collectAsState()
var currentColor by remember { mutableStateOf<Color?>(null) }
LaunchedEffect(currentDisplayRefresh) {
if (currentDisplayRefresh) {
delay(1.5.seconds)
hostState.currentDisplayRefresh = false
if (!currentDisplayRefresh) {
currentColor = null
return@LaunchedEffect
}
val refreshDurationHalf = refreshDuration.milliseconds / 2
currentColor = if (flashMode == ReaderPreferences.FlashColor.BLACK) {
Color.Black
} else {
Color.White
}
delay(refreshDurationHalf)
if (flashMode == ReaderPreferences.FlashColor.WHITE_BLACK) {
currentColor = Color.Black
}
delay(refreshDurationHalf)
hostState.currentDisplayRefresh = false
}
LaunchedEffect(flashInterval) {
hostState.setInterval(flashInterval)
}
Canvas(
modifier = modifier.fillMaxSize(),
) {
if (currentDisplayRefresh) {
drawRect(Color.Black)
}
currentColor?.let { drawRect(it) }
}
}
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
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
@@ -31,9 +32,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 <--
@@ -62,6 +63,25 @@ fun ReaderPageActionsDialog(
icon = Icons.Outlined.Photo,
onClick = { showSetCoverDialog = true },
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(
// SY -->
if (hasExtraPage) {
SYMR.strings.action_copy_first_page
} else {
MR.strings.action_copy_to_clipboard
},
// SY <--
),
icon = Icons.Outlined.ContentCopy,
onClick = {
// SY -->
onShare(true, false)
// SY <--
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(
@@ -76,7 +96,7 @@ fun ReaderPageActionsDialog(
icon = Icons.Outlined.Share,
onClick = {
// SY -->
onShare(false)
onShare(false, false)
// SY <--
onDismissRequest()
},
@@ -114,12 +134,21 @@ fun ReaderPageActionsDialog(
showSetCoverDialog = true
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(SYMR.strings.action_copy_second_page),
icon = Icons.Outlined.ContentCopy,
onClick = {
onShare(true, true)
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(SYMR.strings.action_share_second_page),
icon = Icons.Outlined.Share,
onClick = {
onShare(true)
onShare(false, true)
onDismissRequest()
},
)
@@ -136,12 +165,21 @@ fun ReaderPageActionsDialog(
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(SYMR.strings.action_copy_combined_page),
icon = Icons.Outlined.ContentCopy,
onClick = {
onShareCombined(true)
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(SYMR.strings.action_share_combined_page),
icon = Icons.Outlined.Share,
onClick = {
onShareCombined()
onShareCombined(false)
onDismissRequest()
},
)
@@ -5,11 +5,14 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@@ -20,9 +23,27 @@ private val themes = listOf(
MR.strings.automatic_background to 3,
)
private val flashColors = listOf(
MR.strings.pref_flash_style_black to ReaderPreferences.FlashColor.BLACK,
MR.strings.pref_flash_style_white to ReaderPreferences.FlashColor.WHITE,
MR.strings.pref_flash_style_white_black to ReaderPreferences.FlashColor.WHITE_BLACK,
)
@Composable
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
val flashPageState by screenModel.preferences.flashOnPageChange().collectAsState()
val flashMillisPref = screenModel.preferences.flashDurationMillis()
val flashMillis by flashMillisPref.collectAsState()
val flashIntervalPref = screenModel.preferences.flashPageInterval()
val flashInterval by flashIntervalPref.collectAsState()
val flashColorPref = screenModel.preferences.flashColor()
val flashColor by flashColorPref.collectAsState()
SettingsChipRow(MR.strings.pref_reader_theme) {
themes.map { (labelRes, value) ->
FilterChip(
@@ -95,6 +116,35 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
label = stringResource(MR.strings.pref_flash_page),
pref = screenModel.preferences.flashOnPageChange(),
)
if (flashPageState) {
SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
min = 1,
max = 15,
)
SliderItem(
value = flashInterval,
label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = {
flashIntervalPref.set(it)
},
min = 1,
max = 10,
)
SettingsChipRow(MR.strings.pref_flash_with) {
flashColors.map { (labelRes, value) ->
FilterChip(
selected = flashColor == value,
onClick = { flashColorPref.set(value) },
label = { Text(stringResource(labelRes)) },
)
}
}
}
// SY -->
CheckboxItem(
@@ -19,8 +19,8 @@ internal object NordColorScheme : BaseColorScheme() {
inversePrimary = Color(0xFF397E91),
secondary = Color(0xFF81A1C1), // Unread badge
onSecondary = Color(0xFF2E3440), // Unread badge text
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
secondaryContainer = Color(0xFF506275), // Navigation bar selector pill & progress indicator (remaining)
onSecondaryContainer = Color(0xFF88C0D0), // Navigation bar selector icon
tertiary = Color(0xFF5E81AC), // Downloaded badge
onTertiary = Color(0xFF000000), // Downloaded badge text
tertiaryContainer = Color(0xFF5E81AC),
@@ -54,7 +54,7 @@ internal object NordColorScheme : BaseColorScheme() {
inversePrimary = Color(0xFF8CA8CD),
secondary = Color(0xFF81A1C1), // Unread badge
onSecondary = Color(0xFF2E3440), // Unread badge text
secondaryContainer = Color(0xFF81A1C1), // Navigation bar selector pill & progress indicator (remaining)
secondaryContainer = Color(0xFF91B4D7), // Navigation bar selector pill & progress indicator (remaining)
onSecondaryContainer = Color(0xFF2E3440), // Navigation bar selector icon
tertiary = Color(0xFF88C0D0), // Downloaded badge
onTertiary = Color(0xFF2E3440), // Downloaded badge text
@@ -72,6 +72,7 @@ fun TrackInfoDialogHome(
onNewSearch: (TrackItem) -> Unit,
onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit,
) {
Column(
modifier = Modifier
@@ -116,6 +117,7 @@ fun TrackInfoDialogHome(
onNewSearch = { onNewSearch(item) },
onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) },
)
} else {
TrackInfoItemEmpty(
@@ -144,6 +146,7 @@ private fun TrackInfoItem(
onNewSearch: () -> Unit,
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
) {
val context = LocalContext.current
Column {
@@ -153,6 +156,7 @@ private fun TrackInfoItem(
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
Box(
modifier = Modifier
@@ -179,6 +183,7 @@ private fun TrackInfoItem(
TrackInfoItemMenu(
onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved,
onCopyLink = onCopyLink,
)
}
@@ -287,6 +292,7 @@ private fun TrackInfoItemEmpty(
private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@@ -307,6 +313,13 @@ private 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 = {
@@ -56,6 +56,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
)
}
@@ -71,6 +72,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
)
}
@@ -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,
)
}
}
+17 -8
View File
@@ -199,30 +199,39 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
)
}
@Suppress("MagicNumber")
override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(this).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
components {
// NetworkFetcher.Factory
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
// Decoder.Factory
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
add(MangaKeyer())
add(MangaCoverKeyer())
// Fetcher.Factory
add(BufferedSourceFetcher.Factory())
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
// SY -->
add(PagePreviewKeyer())
add(PagePreviewFetcher.Factory(callFactoryLazy))
// SY <--
// Keyer
add(MangaCoverKeyer())
add(MangaKeyer())
// SY -->
add(PagePreviewKeyer())
// SY <--
}
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
// Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
}.build()
fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8))
decoderCoroutineContext(Dispatchers.IO.limitedParallelism(3))
}
.build()
}
override fun onStart(owner: LifecycleOwner) {
@@ -3,19 +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 eu.kanade.tachiyomi.data.backup.models.BackupSerializer
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.
*/
@@ -27,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(BackupSerializer, 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`
}
}
@@ -6,16 +6,17 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import kotlinx.serialization.protobuf.ProtoBuf
@@ -51,6 +52,7 @@ class BackupCreator(
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(),
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
// SY -->
private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(),
@@ -62,47 +64,49 @@ class BackupCreator(
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)
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() }
// 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)
}
)
// 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 databaseManga = getFavorites.await() /* SY --> */ +
if (options.readEntries) {
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
} else {
emptyList()
} + getMergedManga.await() // SY <--
val backupManga = backupMangas(
getFavorites.await() /* SY --> */ +
if (options.readEntries) {
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
} else {
emptyList()
} + getMergedManga.await(), // SY <--
options
)
val backup = Backup(
backupManga = backupMangas(databaseManga, options),
backupManga = backupManga,
backupCategories = backupCategories(options),
backupSources = backupSources(databaseManga),
backupSources = backupSources(backupManga),
backupPreferences = backupAppPreferences(options),
backupExtensionRepo = backupExtensionRepos(options),
backupSourcePreferences = backupSourcePreferences(options),
// SY -->
backupSavedSearches = backupSavedSearches(),
backupSavedSearches = backupSavedSearches(options),
// SY <--
)
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
val byteArray = parser.encodeToByteArray(Backup.serializer(), backup)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
}
@@ -135,32 +139,42 @@ class BackupCreator(
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
if (!options.categories) return emptyList()
return categoriesBackupCreator.backupCategories()
return categoriesBackupCreator()
}
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
return mangaBackupCreator.backupMangas(mangas, options)
if (!options.libraryEntries) return emptyList()
return mangaBackupCreator(mangas, options)
}
fun backupSources(mangas: List<Manga>): List<BackupSource> {
return sourcesBackupCreator.backupSources(mangas)
fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
return sourcesBackupCreator(mangas)
}
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
if (!options.appSettings) return emptyList()
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
}
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
if (!options.sourceSettings) return emptyList()
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings)
}
suspend fun backupExtensionRepos(options: BackupOptions): List<BackupExtensionRepos> {
if (!options.extensionRepoSettings) return emptyList()
return extensionRepoBackupCreator()
}
// SY -->
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
return savedSearchBackupCreator.backupSavedSearches()
suspend fun backupSavedSearches(options: BackupOptions): List<BackupSavedSearch> {
if (!options.savedSearches) return emptyList()
return savedSearchBackupCreator()
}
// SY <--
@@ -12,11 +12,13 @@ data class BackupOptions(
val tracking: Boolean = true,
val history: Boolean = true,
val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
// SY -->
val customInfo: Boolean = true,
val readEntries: Boolean = true,
val savedSearches: Boolean = true,
// SY <--
) {
@@ -27,15 +29,17 @@ data class BackupOptions(
tracking,
history,
appSettings,
extensionRepoSettings,
sourceSettings,
privateSettings,
// SY -->
customInfo,
readEntries,
savedSearches,
// SY <--
)
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches
companion object {
val libraryOptions = persistentListOf(
@@ -44,12 +48,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,
getter = BackupOptions::chapters,
@@ -68,6 +66,11 @@ 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) },
),
// SY -->
Entry(
label = SYMR.strings.custom_entry_info,
@@ -81,6 +84,11 @@ data class BackupOptions(
setter = { options, enabled -> options.copy(readEntries = enabled) },
enabled = { it.libraryEntries },
),
Entry(
label = SYMR.strings.saved_searches,
getter = BackupOptions::savedSearches,
setter = { options, enabled -> options.copy(savedSearches = enabled) },
),
// SY <--
)
@@ -90,6 +98,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,
@@ -110,11 +123,13 @@ data class BackupOptions(
tracking = array[3],
history = array[4],
appSettings = array[5],
sourceSettings = array[6],
privateSettings = array[7],
extensionRepoSettings = array[6],
sourceSettings = array[7],
privateSettings = array[8],
// SY -->
customInfo = array[8],
readEntries = array[9],
customInfo = array[9],
readEntries = array[10],
savedSearches = array[11],
// SY <--
)
}
@@ -11,7 +11,7 @@ class CategoriesBackupCreator(
private val getCategories: GetCategories = Injekt.get(),
) {
suspend fun backupCategories(): List<BackupCategory> {
suspend operator fun invoke(): List<BackupCategory> {
return getCategories.await()
.filterNot(Category::isSystemCategory)
.map(backupCategoryMapper)
@@ -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.interactor.GetExtensionRepo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionRepoBackupCreator(
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
) {
suspend operator fun invoke(): List<BackupExtensionRepos> {
return getExtensionRepos.getAll()
.map(backupExtensionReposMapper)
}
}
@@ -34,7 +34,7 @@ class MangaBackupCreator(
// SY <--
) {
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
suspend operator fun invoke(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
return mangas.map {
backupManga(it, options)
}
@@ -22,12 +22,12 @@ class PreferenceBackupCreator(
private val preferenceStore: PreferenceStore = Injekt.get(),
) {
fun backupAppPreferences(includePrivatePreferences: Boolean): List<BackupPreference> {
fun createApp(includePrivatePreferences: Boolean): List<BackupPreference> {
return preferenceStore.getAll().toBackupPreferences()
.withPrivatePreferences(includePrivatePreferences)
}
fun backupSourcePreferences(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>()
.map {
@@ -10,7 +10,7 @@ class SavedSearchBackupCreator(
private val handler: DatabaseHandler = Injekt.get()
) {
suspend fun backupSavedSearches(): List<BackupSavedSearch> {
suspend operator fun invoke(): List<BackupSavedSearch> {
return handler.awaitList { saved_searchQueries.selectAll(backupSavedSearchMapper) }
}
}
@@ -1,8 +1,8 @@
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.Source
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -11,10 +11,10 @@ class SourcesBackupCreator(
private val sourceManager: SourceManager = Injekt.get(),
) {
fun backupSources(mangas: List<Manga>): List<BackupSource> {
operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
return mangas
.asSequence()
.map(Manga::source)
.map(BackupManga::source)
.distinct()
.map(sourceManager::getOrStub)
.map { it.toBackupSource() }
@@ -1,12 +1,9 @@
package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.protobuf.ProtoNumber
@Serializer(forClass = Backup::class)
object BackupSerializer
@Suppress("MagicNumber")
@Serializable
data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>,
@@ -15,6 +12,7 @@ data class Backup(
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
@ProtoNumber(106) var backupExtensionRepo: List<BackupExtensionRepos> = emptyList(),
// SY specific values
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
)
@@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import mihon.domain.extensionrepo.model.ExtensionRepo
@Suppress("MagicNumber")
@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,
)
}
@@ -5,11 +5,13 @@ import android.net.Uri
import eu.kanade.tachiyomi.data.backup.BackupDecoder
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
import eu.kanade.tachiyomi.data.backup.restore.restorers.SavedSearchRestorer
@@ -34,6 +36,7 @@ class BackupRestorer(
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(),
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
// SY -->
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
@@ -74,8 +77,11 @@ class BackupRestorer(
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
sourceMapping = backupMaps.associate { it.sourceId to it.name }
if (options.library) {
restoreAmount += backup.backupManga.size + 1 // +1 for categories
if (options.libraryEntries) {
restoreAmount += backup.backupManga.size
}
if (options.categories) {
restoreAmount += 1
}
// SY -->
if (options.savedSearches) {
@@ -85,12 +91,15 @@ class BackupRestorer(
if (options.appSettings) {
restoreAmount += 1
}
if (options.extensionRepoSettings) {
restoreAmount += backup.backupExtensionRepo.size
}
if (options.sourceSettings) {
restoreAmount += 1
}
coroutineScope {
if (options.library) {
if (options.categories) {
restoreCategories(backup.backupCategories)
}
// SY -->
@@ -104,8 +113,11 @@ class BackupRestorer(
if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences)
}
if (options.library) {
restoreManga(backup.backupManga, backup.backupCategories)
if (options.libraryEntries) {
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
}
if (options.extensionRepoSettings) {
restoreExtensionRepos(backup.backupExtensionRepo)
}
// TODO: optionally trigger online library + tracker update
@@ -114,7 +126,7 @@ class BackupRestorer(
private fun CoroutineScope.restoreCategories(backupCategories: List<BackupCategory>) = launch {
ensureActive()
categoriesRestorer.restoreCategories(backupCategories)
categoriesRestorer(backupCategories)
restoreProgress += 1
notifier.showRestoreProgress(
@@ -150,7 +162,7 @@ class BackupRestorer(
ensureActive()
try {
mangaRestorer.restoreManga(it, backupCategories)
mangaRestorer.restore(it, backupCategories)
} catch (e: Exception) {
val sourceName = sourceMapping[it.source] ?: it.source.toString()
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
@@ -163,7 +175,7 @@ class BackupRestorer(
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
ensureActive()
preferenceRestorer.restoreAppPreferences(preferences)
preferenceRestorer.restoreApp(preferences)
restoreProgress += 1
notifier.showRestoreProgress(
@@ -176,7 +188,7 @@ class BackupRestorer(
private fun CoroutineScope.restoreSourcePreferences(preferences: List<BackupSourcePreferences>) = launch {
ensureActive()
preferenceRestorer.restoreSourcePreferences(preferences)
preferenceRestorer.restoreSource(preferences)
restoreProgress += 1
notifier.showRestoreProgress(
@@ -187,10 +199,33 @@ class BackupRestorer(
)
}
private fun CoroutineScope.restoreExtensionRepos(
backupExtensionRepo: List<BackupExtensionRepos>
) = launch {
backupExtensionRepo
.forEach {
ensureActive()
try {
extensionRepoRestorer(it)
} catch (e: Exception) {
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
}
restoreProgress += 1
notifier.showRestoreProgress(
context.stringResource(MR.strings.extensionRepo_settings),
restoreProgress,
restoreAmount,
isSync,
)
}
}
private fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
val file = context.createFileInCacheDir("mihon_restore_error.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
file.bufferedWriter().use { out ->
@@ -6,8 +6,10 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
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,
// SY -->
val savedSearches: Boolean = true,
@@ -15,28 +17,40 @@ data class RestoreOptions(
) {
fun asBooleanArray() = booleanArrayOf(
library,
libraryEntries,
categories,
appSettings,
extensionRepoSettings,
sourceSettings,
// SY -->
savedSearches
savedSearches,
// SY <--
)
fun anyEnabled() = library || appSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */
fun canRestore() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */
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,
@@ -52,11 +66,13 @@ data class RestoreOptions(
)
fun fromBooleanArray(array: BooleanArray) = RestoreOptions(
library = array[0],
appSettings = array[1],
sourceSettings = array[2],
libraryEntries = array[0],
categories = array[1],
appSettings = array[2],
extensionRepoSettings = array[3],
sourceSettings = array[4],
// SY -->
savedSearches = array[3]
savedSearches = array[5]
// SY <--
)
}
@@ -13,7 +13,7 @@ class CategoriesRestorer(
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) {
suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
suspend operator fun invoke(backupCategories: List<BackupCategory>) {
if (backupCategories.isNotEmpty()) {
val dbCategories = getCategories.await()
val dbCategoriesByName = dbCategories.associateBy { it.name }
@@ -21,14 +21,15 @@ class CategoriesRestorer(
val categories = backupCategories
.sortedBy { it.order }
.distinctBy { it.name }
.map {
val newOrder = nextOrder++
dbCategoriesByName[it.name]
?: handler.awaitOneExecutable {
categoriesQueries.insert(it.name, newOrder, it.flags)
categoriesQueries.selectLastInsertedRowId()
}.let { id -> it.toCategory(id).copy(order = newOrder) }
val dbCategory = dbCategoriesByName[it.name]
if (dbCategory != null) return@map dbCategory
val order = nextOrder++
handler.awaitOneExecutable {
categoriesQueries.insert(it.name, order, it.flags)
categoriesQueries.selectLastInsertedRowId()
}
.let { id -> it.toCategory(id).copy(order = order) }
}
libraryPreferences.categorizedDisplaySettings().set(
@@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
import tachiyomi.data.DatabaseHandler
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionRepoRestorer(
private val handler: DatabaseHandler = Injekt.get(),
private val getExtensionRepos: GetExtensionRepo = Injekt.get()
) {
suspend operator fun invoke(
backupRepo: BackupExtensionRepos,
) {
val dbRepos = getExtensionRepos.getAll()
val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint }
val existingReposByUrl = dbRepos.associateBy { it.baseUrl }
val urlExists = existingReposByUrl[backupRepo.baseUrl]
val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint]
if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) {
error("Already Exists with different signing key fingerprint")
} else if (shaExists != null) {
error("${shaExists.name} has the same signing key fingerprint")
} else {
handler.await {
extension_reposQueries.insert(
backupRepo.baseUrl,
backupRepo.name,
backupRepo.shortName,
backupRepo.website,
backupRepo.signingKeyFingerprint
)
}
}
}
}
@@ -68,7 +68,7 @@ class MangaRestorer(
)
}
suspend fun restoreManga(
suspend fun restore(
backupManga: BackupManga,
backupCategories: List<BackupCategory>,
) {
@@ -22,14 +22,14 @@ class PreferenceRestorer(
private val preferenceStore: PreferenceStore = Injekt.get(),
) {
fun restoreAppPreferences(preferences: List<BackupPreference>) {
fun restoreApp(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs)
@@ -21,7 +21,6 @@ import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
@@ -348,5 +347,7 @@ class MangaCoverFetcher(
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
private const val HTTP_NOT_MODIFIED = 304
}
}
@@ -18,7 +18,6 @@ import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
@@ -260,5 +259,7 @@ class PagePreviewFetcher(
companion object {
private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
private const val HTTP_NOT_MODIFIED = 304
}
}
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap
import android.os.Build
import coil3.ImageLoader
import coil3.asCoilImage
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
@@ -80,7 +80,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
}
return DecodeResult(
image = bitmap.asCoilImage(),
image = bitmap.asImage(),
isSampled = sampleSize > 1,
)
}
@@ -21,6 +21,7 @@ import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.model.Chapter
@@ -170,7 +171,7 @@ class DownloadManager(
source,
)
val files = chapterDir?.listFiles().orEmpty()
.filter { "image" in it.type.orEmpty() }
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
if (files.isEmpty()) {
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
@@ -553,14 +553,8 @@ class Downloader(
* @param file the file where the image is already downloaded.
*/
private fun getImageExtension(response: Response, file: UniFile): String {
// Read content type if available.
val mime = response.body.contentType()?.run { if (type == "image") "image/$subtype" else null }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime
return ImageUtil.getExtensionFromMimeType(mime)
return ImageUtil.getExtensionFromMimeType(mime) { file.openInputStream() }
}
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) {
@@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
@@ -65,21 +66,26 @@ class ImageSaver(
filename: String,
data: () -> InputStream,
): Uri {
val pictureDir =
val isMimeTypeSupported = MimeTypeMap.getSingleton().hasMimeType(type.mime)
val pictureDir = if (isMimeTypeSupported) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
}
val imageLocation = (image.location as Location.Pictures).relativePath
val relativePath = listOf(
Environment.DIRECTORY_PICTURES,
if (isMimeTypeSupported) Environment.DIRECTORY_PICTURES else Environment.DIRECTORY_DOCUMENTS,
context.stringResource(MR.strings.app_name),
imageLocation,
).joinToString(File.separator)
val contentValues = contentValuesOf(
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
MediaStore.Images.Media.DISPLAY_NAME to image.name,
MediaStore.Images.Media.MIME_TYPE to type.mime,
MediaStore.Images.Media.DATE_MODIFIED to Instant.now().epochSecond,
MediaStore.MediaColumns.RELATIVE_PATH to relativePath,
MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename,
MediaStore.MediaColumns.MIME_TYPE to type.mime,
MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond,
)
val picture = findUriOrDefault(relativePath, filename) {
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
@@ -85,6 +84,7 @@ class SyncManager(
chapters = syncOptions.chapters,
tracking = syncOptions.tracking,
history = syncOptions.history,
extensionRepoSettings = syncOptions.extensionRepoSettings,
appSettings = syncOptions.appSettings,
sourceSettings = syncOptions.sourceSettings,
privateSettings = syncOptions.privateSettings,
@@ -92,19 +92,22 @@ class SyncManager(
// SY -->
customInfo = syncOptions.customInfo,
readEntries = syncOptions.readEntries,
savedSearches = syncOptions.savedSearches,
// SY <--
)
logcat(LogPriority.DEBUG) { "Begin create backup" }
val backupManga = backupCreator.backupMangas(databaseManga, backupOptions)
val backup = Backup(
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
backupManga = backupManga,
backupCategories = backupCreator.backupCategories(backupOptions),
backupSources = backupCreator.backupSources(databaseManga),
backupSources = backupCreator.backupSources(backupManga),
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions),
// SY -->
backupSavedSearches = backupCreator.backupSavedSearches(),
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
// SY <--
)
logcat(LogPriority.DEBUG) { "End create backup" }
@@ -175,6 +178,7 @@ class SyncManager(
backupSources = remoteBackup.backupSources,
backupPreferences = remoteBackup.backupPreferences,
backupSourcePreferences = remoteBackup.backupSourcePreferences,
backupExtensionRepo = remoteBackup.backupExtensionRepo,
// SY -->
backupSavedSearches = remoteBackup.backupSavedSearches,
@@ -199,7 +203,8 @@ class SyncManager(
options = RestoreOptions(
appSettings = true,
sourceSettings = true,
library = true,
libraryEntries = true,
extensionRepoSettings = true,
),
)
@@ -214,7 +219,7 @@ class SyncManager(
val cacheFile = File(context.cacheDir, "tachiyomi_sync_data.proto.gz")
return try {
cacheFile.outputStream().use { output ->
output.write(ProtoBuf.encodeToByteArray(BackupSerializer, backup))
output.write(ProtoBuf.encodeToByteArray(Backup.serializer(), backup))
Uri.fromFile(cacheFile)
}
} catch (e: IOException) {
@@ -10,7 +10,6 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeToken
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
import com.google.api.client.http.ByteArrayContent
import com.google.api.client.http.InputStreamContent
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.JsonFactory
@@ -20,11 +19,9 @@ import com.google.api.services.drive.DriveScopes
import com.google.api.services.drive.model.File
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.tachiyomi.data.backup.models.Backup
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import logcat.logcat
import tachiyomi.core.common.i18n.stringResource
@@ -37,7 +34,6 @@ import uy.kohesive.injekt.api.get
import java.io.IOException
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.time.Instant
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
@@ -64,12 +60,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
private val appName = context.stringResource(MR.strings.app_name)
private val remoteFileName = "${appName}_sync_data.gz"
private val lockFileName = "${appName}_sync.lock"
private val remoteFileName = "${appName}_sync.proto.gz"
private val googleDriveService = GoogleDriveService(context)
private val protoBuf: ProtoBuf = Injekt.get()
override suspend fun doSync(syncData: SyncData): Backup? {
beforeSync()
@@ -107,64 +103,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
}
private suspend fun beforeSync() {
try {
googleDriveService.refreshToken()
val drive = googleDriveService.driveService
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
var backoff = 1000L
var retries = 0 // Retry counter
val maxRetries = 10 // Maximum number of retries
while (retries < maxRetries) {
val lockFiles = findLockFile(drive)
logcat(LogPriority.DEBUG) { "Found ${lockFiles.size} lock file(s)" }
when {
lockFiles.isEmpty() -> {
logcat(LogPriority.DEBUG) { "No lock file found, creating a new one" }
createLockFile(drive)
break
}
lockFiles.size == 1 -> {
val lockFile = lockFiles.first()
val createdTime = Instant.parse(lockFile.createdTime.toString())
val ageMinutes = java.time.Duration.between(createdTime, Instant.now()).toMinutes()
logcat(LogPriority.DEBUG) { "Lock file age: $ageMinutes minutes" }
if (ageMinutes <= 3) {
logcat(LogPriority.DEBUG) { "Lock file is new, proceeding with sync" }
break
} else {
logcat(LogPriority.DEBUG) { "Lock file is old, deleting and creating a new one" }
deleteLockFile(drive)
createLockFile(drive)
break
}
}
else -> {
logcat(LogPriority.DEBUG) { "Multiple lock files found, applying backoff" }
delay(backoff) // Apply backoff strategy
backoff = (backoff * 2).coerceAtMost(16000L)
logcat(LogPriority.DEBUG) { "Backoff increased to $backoff milliseconds" }
}
}
retries++ // Increment retry counter
logcat(LogPriority.DEBUG) { "Loop iteration complete, retry count: $retries, backoff time: $backoff" }
}
if (retries >= maxRetries) {
logcat(LogPriority.ERROR) { "Max retries reached, exiting sync process" }
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": Max retries reached.")
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" }
throw Exception(context.stringResource(SYMR.strings.error_before_sync_gdrive) + ": ${e.message}", e)
}
googleDriveService.refreshToken()
}
private fun pullSyncData(): SyncData? {
val drive = googleDriveService.driveService ?:
throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
val drive = googleDriveService.driveService
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
val fileList = getAppDataFileList(drive)
if (fileList.isEmpty()) {
@@ -178,7 +122,10 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
try {
drive.files().get(gdriveFileId).executeMediaAsInputStream().use { inputStream ->
GZIPInputStream(inputStream).use { gzipInputStream ->
return Json.decodeFromStream(SyncData.serializer(), gzipInputStream)
val byteArray = gzipInputStream.readBytes()
val backup = protoBuf.decodeFromByteArray(Backup.serializer(), byteArray)
val deviceId = fileList[0].appProperties["deviceId"] ?: ""
return SyncData(deviceId = deviceId, backup = backup)
}
}
} catch (e: Exception) {
@@ -192,29 +139,40 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
?: throw Exception(context.stringResource(SYMR.strings.google_drive_not_signed_in))
val fileList = getAppDataFileList(drive)
val backup = syncData.backup ?: return
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
}
PipedOutputStream().use { pos ->
PipedInputStream(pos).use { pis ->
withIOContext {
// Start a coroutine or a background thread to write JSON to the PipedOutputStream
launch {
GZIPOutputStream(pos).use { gzipOutputStream ->
Json.encodeToStream(SyncData.serializer(), syncData, gzipOutputStream)
gzipOutputStream.write(byteArray)
}
}
val mediaContent = InputStreamContent("application/octet-stream", pis)
if (fileList.isNotEmpty()) {
val fileId = fileList[0].id
val mediaContent = InputStreamContent("application/gzip", pis)
drive.files().update(fileId, null, mediaContent).execute()
val fileMetadata = File().apply {
name = remoteFileName
mimeType = "application/octet-stream"
appProperties = mapOf("deviceId" to syncData.deviceId)
}
drive.files().update(fileId, fileMetadata, mediaContent).execute()
logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" }
} else {
val fileMetadata = File().apply {
name = remoteFileName
mimeType = "application/gzip"
mimeType = "application/octet-stream"
parents = listOf("appDataFolder")
appProperties = mapOf("deviceId" to syncData.deviceId)
}
val mediaContent = InputStreamContent("application/gzip", pis)
val uploadedFile = drive.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute()
@@ -228,12 +186,12 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
private fun getAppDataFileList(drive: Drive): MutableList<File> {
try {
// Search for the existing file by name in the appData folder
val query = "mimeType='application/gzip' and name = '$remoteFileName'"
val query = "mimeType='application/x-gzip' and name = '$remoteFileName'"
val fileList = drive.files()
.list()
.setSpaces("appDataFolder")
.setQ(query)
.setFields("files(id, name, createdTime)")
.setFields("files(id, name, createdTime, appProperties)")
.execute()
.files
logcat { "AppData folder file list: $fileList" }
@@ -245,62 +203,6 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
}
}
private fun createLockFile(drive: Drive) {
try {
val fileMetadata = File().apply {
name = lockFileName
mimeType = "text/plain"
parents = listOf("appDataFolder")
}
// Create an empty content to upload as the lock file
val emptyContent = ByteArrayContent.fromString("text/plain", "")
val file = drive.files().create(fileMetadata, emptyContent)
.setFields("id, name, createdTime")
.execute()
logcat { "Created lock file with ID: ${file.id}" }
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) { "Error creating lock file" }
throw Exception(e.message, e)
}
}
private fun findLockFile(drive: Drive): MutableList<File> {
return try {
val query = "mimeType='text/plain' and name = '$lockFileName'"
val fileList = drive.files()
.list()
.setSpaces("appDataFolder")
.setQ(query)
.setFields("files(id, name, createdTime)")
.execute().files
logcat { "Lock file search result: $fileList" }
fileList
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) { "Error finding lock file" }
mutableListOf()
}
}
private fun deleteLockFile(drive: Drive) {
try {
val lockFiles = findLockFile(drive)
if (lockFiles.isNotEmpty()) {
for (file in lockFiles) {
drive.files().delete(file.id).execute()
logcat { "Deleted lock file with ID: ${file.id}" }
}
} else {
logcat { "No lock file found to delete." }
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" }
throw Exception(context.stringResource(SYMR.strings.error_deleting_google_drive_lock_file), e)
}
}
suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus {
val drive = googleDriveService.driveService
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.sync.service
import android.content.Context
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.sync.SyncNotifier
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.PUT
@@ -104,7 +103,7 @@ class SyncYomiSyncService(
}
return try {
val backup = protoBuf.decodeFromByteArray(BackupSerializer, byteArray)
val backup = protoBuf.decodeFromByteArray(Backup.serializer(), byteArray)
return Pair(SyncData(backup = backup), newETag)
} catch (_: SerializationException) {
logcat(LogPriority.INFO) {
@@ -147,7 +146,7 @@ class SyncYomiSyncService(
.writeTimeout(timeout, TimeUnit.SECONDS)
.build()
val byteArray = protoBuf.encodeToByteArray(BackupSerializer, backup)
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
}
@@ -8,6 +8,8 @@ import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import logcat.LogPriority
import okhttp3.OkHttpClient
import tachiyomi.core.common.util.lang.withIOContext
@@ -53,6 +55,15 @@ abstract class BaseTracker(
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
override val isLoggedInFlow: Flow<Boolean> by lazy {
combine(
trackPreferences.trackUsername(this).changes(),
trackPreferences.trackPassword(this).changes(),
) { username, password ->
username.isNotEmpty() && password.isNotEmpty()
}
}
override fun getUsername() = trackPreferences.trackUsername(this).get()
override fun getPassword() = trackPreferences.trackPassword(this).get()
@@ -7,6 +7,7 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
import okhttp3.OkHttpClient
import tachiyomi.domain.track.model.Track as DomainTrack
@@ -61,6 +62,8 @@ interface Tracker {
val isLoggedIn: Boolean
val isLoggedInFlow: Flow<Boolean>
fun getUsername(): String
fun getPassword(): String
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
import kotlinx.coroutines.flow.combine
class TrackerManager {
@@ -40,5 +41,13 @@ class TrackerManager {
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
fun loggedInTrackersFlow() = combine(trackers.map { it.isLoggedInFlow }) {
it.mapIndexedNotNull { index, isLoggedIn ->
if (isLoggedIn) trackers[index] else null
}
}
fun get(id: Long) = trackers.find { it.id == id }
fun getAll(ids: Set<Long>) = trackers.filter { it.id in ids }
}
@@ -270,10 +270,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
private const val clientSecret =
"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 algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/"
private const val baseUrl = "https://kitsu.app/api/edge/"
private const val loginUrl = "https://kitsu.app/api/oauth/token"
private const val baseMangaUrl = "https://kitsu.app/manga/"
private const val algoliaKeyUrl = "https://kitsu.app/api/edge/algolia-keys/media/"
private const val algoliaUrl =
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
@@ -129,12 +129,7 @@ class MyAnimeListApi(
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) {
""
}
start_date = obj["start_date"]?.jsonPrimitive?.content ?: ""
}
}
}
@@ -102,7 +102,7 @@ abstract class Installer(private val service: Service) {
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeFirst()
queue.removeAt(0)
processEntry(nextEntry)
}
}
@@ -133,7 +133,10 @@ internal class ExtensionInstaller(private val context: Context) {
emit(downloadStatus)
// Stop polling when the download fails or finishes
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
if (
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
downloadStatus == DownloadManager.STATUS_FAILED
) {
return@flow
}
@@ -53,6 +53,9 @@ class AndroidSourceManager(
private val sourceRepository: StubSourceRepository,
) : SourceManager {
private val _isInitialized = MutableStateFlow(false)
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val downloadManager: DownloadManager by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.IO)
@@ -189,9 +192,6 @@ class AndroidSourceManager(
}
// SY -->
private val _isInitialized = MutableStateFlow(false)
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
override fun getVisibleOnlineSources() = sourcesMapFlow.value.values
.filterIsInstance<HttpSource>()
.filter {
@@ -74,6 +74,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
binding.skipStep.isChecked = preferences.skipPreMigration().get()
binding.HideNotFoundManga.isChecked = preferences.hideNotFoundMigration().get()
binding.OnlyShowUpdates.isChecked = preferences.showOnlyUpdatesMigration().get()
binding.skipStep.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding.root.context.toast(
@@ -86,6 +87,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
binding.migrateBtn.setOnClickListener {
preferences.skipPreMigration().set(binding.skipStep.isChecked)
preferences.hideNotFoundMigration().set(binding.HideNotFoundManga.isChecked)
preferences.showOnlyUpdatesMigration().set(binding.OnlyShowUpdates.isChecked)
onStartMigration.value(
if (binding.useSmartSearch.isChecked && binding.extraSearchParamText.text.isNotBlank()) {
binding.extraSearchParamText.toString()
@@ -94,6 +94,7 @@ class MigrationListScreenModel(
val manualMigrations = MutableStateFlow(0)
val hideNotFound = preferences.hideNotFoundMigration().get()
val showOnlyUpdates = preferences.showOnlyUpdatesMigration().get()
val navigateOut = MutableSharedFlow<Unit>()
@@ -313,6 +314,12 @@ class MigrationListScreenModel(
if (result == null && hideNotFound) {
removeManga(manga)
}
if (result != null && showOnlyUpdates &&
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
) {
removeManga(manga)
}
sourceFinished()
}
}
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import tachiyomi.core.common.preference.toggle
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
@@ -39,6 +41,7 @@ abstract class SearchScreenModel(
private val extensionManager: ExtensionManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<SearchScreenModel.State>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
@@ -61,6 +64,14 @@ abstract class SearchScreenModel(
)
}
init {
screenModelScope.launch {
preferences.globalSearchFilterState().changes().collectLatest { state ->
mutableState.update { it.copy(onlyShowHasResults = state) }
}
}
}
@Composable
fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
return produceState(initialValue = initialManga) {
@@ -111,7 +122,7 @@ abstract class SearchScreenModel(
}
fun toggleFilterResults() {
mutableState.update { it.copy(onlyShowHasResults = !it.onlyShowHasResults) }
preferences.globalSearchFilterState().toggle()
}
fun search() {
@@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -177,18 +178,18 @@ class LibraryScreenModel(
::Pair,
),
// SY <--
) { searchQuery, library, tracks, (loggedInTrackers, _), (groupType, sort) ->
) { searchQuery, library, tracks, (trackingFiler, _), (groupType, sort) ->
library
// SY -->
.applyGrouping(groupType)
// SY <--
.applyFilters(tracks, loggedInTrackers)
.applySort(tracks, /* SY --> */sort.takeIf { groupType != LibraryGroup.BY_DEFAULT } /* SY <-- */)
.applyFilters(tracks, trackingFiler)
.applySort(tracks, trackingFiler.keys,/* SY --> */sort.takeIf { groupType != LibraryGroup.BY_DEFAULT } /* SY <-- */)
.mapValues { (_, value) ->
if (searchQuery != null) {
// Filter query
// SY -->
filterLibrary(value, searchQuery, loggedInTrackers)
filterLibrary(value, searchQuery, trackingFiler)
// SY <--
} else {
// Don't do anything
@@ -277,9 +278,10 @@ class LibraryScreenModel(
/**
* Applies library filters to the given map of manga.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>,
loggedInTrackers: Map<Long, TriState>,
trackingFiler: Map<Long, TriState>,
): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded
@@ -291,10 +293,10 @@ class LibraryScreenModel(
val filterCompleted = prefs.filterCompleted
val filterIntervalCustom = prefs.filterIntervalCustom
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty()
val isNotLoggedInAnyTrack = trackingFiler.isEmpty()
val excludedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
// SY -->
@@ -371,9 +373,11 @@ class LibraryScreenModel(
/**
* Applies library sorting to the given map of manga.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun LibraryMap.applySort(
// Map<MangaId, List<Track>>
trackMap: Map<Long, List<Track>>,
loggedInTrackerIds: Set<Long>,
/* SY --> */
groupSort: LibrarySort? = null, /* SY <-- */
): LibraryMap {
@@ -397,7 +401,7 @@ class LibraryScreenModel(
val defaultTrackerScoreSortValue = -1.0
val trackerScores by lazy {
val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id }
val trackerMap = trackerManager.getAll(loggedInTrackerIds).associateBy { e -> e.id }
trackMap.mapValues { entry ->
when {
entry.value.isEmpty() -> null
@@ -596,18 +600,17 @@ class LibraryScreenModel(
* @return map of track id with the filter value
*/
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
val loggedInTrackers = trackerManager.loggedInTrackers()
return if (loggedInTrackers.isNotEmpty()) {
val prefFlows = loggedInTrackers
.map { libraryPreferences.filterTracking(it.id.toInt()).changes() }
.toTypedArray()
combine(*prefFlows) {
return trackerManager.loggedInTrackersFlow().flatMapLatest { loggedInTrackers ->
if (loggedInTrackers.isEmpty()) return@flatMapLatest flowOf(emptyMap())
val prefFlows = loggedInTrackers.map { tracker ->
libraryPreferences.filterTracking(tracker.id.toInt()).changes()
}
combine(prefFlows) {
loggedInTrackers
.mapIndexed { index, tracker -> tracker.id to it[index] }
.toMap()
}
} else {
flowOf(emptyMap())
}
}
@@ -6,6 +6,8 @@ import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getAndSet
@@ -18,17 +20,22 @@ import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
class LibrarySettingsScreenModel(
val preferences: BasePreferences = Injekt.get(),
val libraryPreferences: LibraryPreferences = Injekt.get(),
private val setDisplayMode: SetDisplayMode = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
trackerManager: TrackerManager = Injekt.get(),
) : ScreenModel {
val trackers
get() = trackerManager.trackers.filter { it.isLoggedIn }
val trackersFlow = trackerManager.loggedInTrackersFlow()
.stateIn(
scope = screenModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = trackerManager.loggedInTrackers()
)
// SY -->
val grouping by libraryPreferences.groupLibraryBy().asState(screenModelScope)
@@ -5,6 +5,7 @@ import android.net.Uri
import androidx.compose.material3.SnackbarHostState
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.size.Size
@@ -186,7 +186,7 @@ class MangaScreen(
)
}.takeIf { isHttpSource },
onTrackingClicked = {
if (screenModel.loggedInTrackers.isEmpty()) {
if (!successState.hasLoggedInTrackers) {
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
} else {
screenModel.showTrackDialog()
@@ -127,6 +127,7 @@ import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.source.local.LocalSource
@@ -187,8 +188,6 @@ class MangaScreenModel(
private val successState: State.Success?
get() = state.value as? State.Success
val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } }
val manga: Manga?
get() = successState?.manga
@@ -1490,45 +1489,56 @@ class MangaScreenModel(
val manga = state?.manga ?: return
screenModelScope.launchIO {
getTracks.subscribe(manga.id)
.catch { logcat(LogPriority.ERROR, it) }
.map { tracks ->
loggedInTrackers
// Map to TrackItem
.map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) }
// Show only if the service supports this manga's source
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
}
// SY -->
.map { trackItems ->
if (manga.source in mangaDexSourceIds || state.mergedData?.manga?.values.orEmpty().any {
it.source in mangaDexSourceIds
}
) {
val mdTrack = trackItems.firstOrNull { it.tracker is MdList }
when {
mdTrack == null -> {
trackItems
combine(
getTracks.subscribe(manga.id)
// SY -->
.map { trackItems ->
if (manga.source in mangaDexSourceIds || state.mergedData?.manga?.values.orEmpty().any {
it.source in mangaDexSourceIds
}
mdTrack.track == null -> {
trackItems - mdTrack + createMdListTrack()
) {
val mdTrack = trackItems.firstOrNull { it.trackerId == TrackerManager.MDLIST }
when {
trackerManager.mdList.isLoggedIn && mdTrack == null -> {
trackItems + createMdListTrack()
}
else -> trackItems
}
else -> trackItems
} else {
trackItems
}
} else {
trackItems
}
// SY <--
.catch { logcat(LogPriority.ERROR, it) },
trackerManager.loggedInTrackersFlow(),
) { mangaTracks, loggedInTrackers ->
// Show only if the service supports this manga's source
val supportedTrackers = loggedInTrackers.filter { (it as? EnhancedTracker)?.accept(source!!) ?: true }
val supportedTrackerIds = supportedTrackers.map { it.id }.toHashSet()
val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds }
// SY -->
val trackingCount = supportedTrackerTracks.count {
(it.trackerId == TrackerManager.MDLIST && it.status != FollowStatus.UNFOLLOWED.long) ||
it.trackerId != TrackerManager.MDLIST
}
trackingCount to supportedTrackers.isNotEmpty()
// SY <--
}
.distinctUntilChanged()
.collectLatest { trackItems ->
updateSuccessState { it.copy(trackItems = trackItems) }
.collectLatest { (trackingCount, hasLoggedInTrackers) ->
updateSuccessState {
it.copy(
trackingCount = trackingCount,
hasLoggedInTrackers = hasLoggedInTrackers,
)
}
}
}
}
// SY -->
private suspend fun createMdListTrack(): TrackItem {
private suspend fun createMdListTrack(): Track {
val state = successState!!
val mdManga = state.manga.takeIf { it.source in mangaDexSourceIds }
?: state.mergedData?.manga?.values?.find { it.source in mangaDexSourceIds }
@@ -1536,7 +1546,7 @@ class MangaScreenModel(
val track = trackerManager.mdList.createInitialTracker(state.manga, mdManga)
.toDomainTrack(false)!!
insertTrack.await(track)
return TrackItem(getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }, trackerManager.mdList)
return getTracks.await(mangaId).first { it.trackerId == trackerManager.mdList.id }
}
// SY <--
@@ -1633,7 +1643,8 @@ class MangaScreenModel(
val chapters: List<ChapterList.Item>,
val availableScanlators: ImmutableSet<String>,
val excludedScanlators: ImmutableSet<String>,
val trackItems: List<TrackItem> = emptyList(),
val trackingCount: Int = 0,
val hasLoggedInTrackers: Boolean = false,
val isRefreshingData: Boolean = false,
val dialog: MangaScreenModel.Dialog? = null,
val hasPromptedToAddBefore: Boolean = false,
@@ -1689,11 +1700,6 @@ class MangaScreenModel(
val filterActive: Boolean
get() = scanlatorFilterActive || manga.chaptersFiltered()
val trackingCount: Int
get() = trackItems.count {
it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.long) || it.tracker !is MdList)
}
/**
* Applies the view filters to the list of chapters obtained from the database.
* @return an observable of the list of chapters filtered and sorted.
@@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
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
@@ -170,6 +171,7 @@ data class TrackInfoDialogHomeScreen(
),
)
},
onCopyLink = { context.copyTrackerLink(it) },
)
}
@@ -183,6 +185,13 @@ data class TrackInfoDialogHomeScreen(
}
}
private fun Context.copyTrackerLink(trackItem: TrackItem) {
val url = trackItem.track?.remoteUrl ?: return
if (url.isNotBlank()) {
copyToClipboard(url, url)
}
}
private class Model(
private val mangaId: Long,
private val sourceId: Long,
@@ -239,7 +248,7 @@ data class TrackInfoDialogHomeScreen(
}
private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedInTrackers = Injekt.get<TrackerManager>().trackers.filter { it.isLoggedIn }
val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
return loggedInTrackers
// Map to TrackItem
@@ -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
@@ -35,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalConfiguration
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
@@ -269,6 +272,9 @@ class ReaderActivity : BaseActivity() {
is ReaderViewModel.Event.ShareImage -> {
onShareImageResult(event.uri, event.page /* SY --> */, event.secondPage /* SY <-- */)
}
is ReaderViewModel.Event.CopyImage -> {
onCopyImageResult(event.uri)
}
is ReaderViewModel.Event.SetCoverResult -> {
onSetAsCoverResult(event.result)
}
@@ -1100,6 +1106,12 @@ class ReaderActivity : BaseActivity() {
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
}
private fun onCopyImageResult(uri: Uri) {
val clipboardManager = applicationContext.getSystemService<ClipboardManager>() ?: 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].
@@ -1156,7 +1156,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
@@ -1182,7 +1182,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)
@@ -1190,7 +1190,7 @@ class ReaderViewModel @JvmOverloads constructor(
}
// SY -->
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)
@@ -1214,7 +1214,7 @@ 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)
@@ -1382,5 +1382,6 @@ class ReaderViewModel @JvmOverloads constructor(
val page: ReaderPage/* SY --> */,
val secondPage: ReaderPage? = null, /* SY <-- */
) : Event
data class CopyImage(val uri: Uri) : Event
}
}
@@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.app.NotificationCompat
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
@@ -23,6 +23,12 @@ class ReaderPreferences(
fun flashOnPageChange() = preferenceStore.getBoolean("pref_reader_flash", false)
fun flashDurationMillis() = preferenceStore.getInt("pref_reader_flash_duration", MILLI_CONVERSION)
fun flashPageInterval() = preferenceStore.getInt("pref_reader_flash_interval", 1)
fun flashColor() = preferenceStore.getEnum("pref_reader_flash_mode", FlashColor.BLACK)
fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500)
fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true)
@@ -182,6 +188,12 @@ class ReaderPreferences(
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
// SY <--
enum class FlashColor {
BLACK,
WHITE,
WHITE_BLACK
}
enum class TappingInvertMode(
val titleRes: StringResource,
val shouldInvertHorizontal: Boolean = false,
@@ -210,6 +222,8 @@ class ReaderPreferences(
const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25
const val MILLI_CONVERSION = 100
val TapZones = listOf(
MR.strings.label_default,
MR.strings.l_nav,
@@ -19,6 +19,7 @@ import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import coil3.BitmapImage
import coil3.asDrawable
import coil3.dispose
import coil3.imageLoader
import coil3.request.CachePolicy
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
@@ -244,16 +245,7 @@ class PagerPageHolder(
private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource {
// Handle adding a center margin to wide images if requested
if (imageSource2 == null) {
return if (
!ImageUtil.isAnimatedAndSupported(imageSource) &&
ImageUtil.isWideImage(imageSource) &&
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
!viewer.config.imageCropBorders
) {
ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
} else {
imageSource
}
return handleWideImage(imageSource)
}
if (page.fullPage) return imageSource
@@ -268,12 +260,7 @@ class PagerPageHolder(
return imageSource
}
val imageBitmap = try {
ImageDecoder.newInstance(imageSource.inputStream())?.decode()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null
}
val imageBitmap = decodeImage(imageSource)
if (imageBitmap == null) {
imageSource2.close()
page.fullPage = true
@@ -281,23 +268,16 @@ class PagerPageHolder(
logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageSource
}
scope.launch { progressIndicator.setProgress(96) }
val height = imageBitmap.height
val width = imageBitmap.width
if (height < width) {
scope.launch { progressIndicator.setProgress(96) }
if (imageBitmap.height < imageBitmap.width) {
imageSource2.close()
page.fullPage = true
splitDoublePages()
return imageSource
}
val imageBitmap2 = try {
ImageDecoder.newInstance(imageSource2.inputStream())?.decode()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null
}
val imageBitmap2 = decodeImage(imageSource2)
if (imageBitmap2 == null) {
imageSource2.close()
extraPage?.fullPage = true
@@ -306,35 +286,63 @@ class PagerPageHolder(
logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageSource
}
scope.launch { progressIndicator.setProgress(97) }
val height2 = imageBitmap2.height
val width2 = imageBitmap2.width
if (height2 < width2) {
scope.launch { progressIndicator.setProgress(97) }
if (imageBitmap2.height < imageBitmap2.width) {
imageSource2.close()
extraPage?.fullPage = true
page.isolatedPage = true
splitDoublePages()
return imageSource
}
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
val centerMargin = calculateCenterMargin(imageBitmap.height, imageBitmap2.height)
imageSource.close()
imageSource2.close()
val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, centerMargin, viewer.config.pageCanvasColor) {
updateProgress(it)
}
}
private fun handleWideImage(imageSource: BufferedSource): BufferedSource {
return if (
!ImageUtil.isAnimatedAndSupported(imageSource) &&
ImageUtil.isWideImage(imageSource) &&
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
!viewer.config.imageCropBorders
) {
ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
} else {
imageSource
}
}
private fun decodeImage(imageSource: BufferedSource): Bitmap? {
return try {
ImageDecoder.newInstance(imageSource.inputStream())?.decode()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot decode image" }
null
}
}
private fun calculateCenterMargin(height: Int, height2: Int): Int {
return if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
} else {
0
}
}
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, centerMargin, viewer.config.pageCanvasColor) {
scope.launch {
if (it == 100) {
progressIndicator.hide()
} else {
progressIndicator.setProgress(it)
}
private fun updateProgress(progress: Int) {
scope.launch {
if (progress == 100) {
progressIndicator.hide()
} else {
progressIndicator.setProgress(progress)
}
}
}
@@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.delay
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.system.logcat
import kotlin.math.max
/**
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
@@ -231,10 +230,12 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
if (!viewer.config.doublePages) {
// If not in double mode, set up items like before
subItems.forEach {
(it as? ReaderPage)?.shiftedPage = false
subItems.forEach { readerItem ->
if (readerItem is ReaderPage) {
readerItem.shiftedPage = false
}
}
this.joinedItems = subItems.map { Pair<ReaderItem, ReaderItem?>(it, null) }.toMutableList()
this.joinedItems = subItems.map { Pair(it, null) }.toMutableList()
if (viewer is R2LPagerViewer) {
joinedItems.reverse()
}
@@ -242,54 +243,43 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
val pagedItems = mutableListOf<MutableList<ReaderPage?>>()
val otherItems = mutableListOf<ReaderItem>()
pagedItems.add(mutableListOf())
// Step 1: segment the pages and transition pages
subItems.forEach {
when (it) {
subItems.forEach { readerItem ->
when (readerItem) {
is ReaderPage -> {
if (pagedItems.last().lastOrNull() != null &&
pagedItems.last().last()?.chapter?.chapter?.id != it.chapter.chapter.id
) {
if (pagedItems.last().isNotEmpty() && pagedItems.last().last()?.chapter?.chapter?.id != readerItem.chapter.chapter.id) {
pagedItems.add(mutableListOf())
}
pagedItems.last().add(it)
pagedItems.last().add(readerItem)
}
is ChapterTransition -> {
otherItems.add(it)
otherItems.add(readerItem)
pagedItems.add(mutableListOf())
}
}
}
var pagedIndex = 0
val subJoinedItems = mutableListOf<Pair<ReaderItem, ReaderItem?>>()
// Step 2: run through each set of pages
pagedItems.forEach { items ->
items.forEach { it?.shiftedPage = false }
items.forEach {
it?.shiftedPage = false
}
// Step 3: If pages have been shifted,
if (viewer.config.shiftDoublePage) {
val index = items.indexOf(pageToShift)
// Go from the current page and work your way back to the first page,
// or the first page that's a full page.
// This is done in case user tries to shift a page after a full page
val fullPageBeforeIndex = if (index > -1) {
items.take(index).indexOfLast { it?.fullPage == true }
} else {
-1
}.coerceAtLeast(0)
// Add a shifted page to the first place there isnt a full page
run loop@{
var index = items.indexOf(pageToShift)
if (pageToShift?.fullPage == true) {
index = max(0, index - 1)
}
// Go from the current page and work your way back to the first page,
// or the first page that's a full page.
// This is done in case user tries to shift a page after a full page
val fullPageBeforeIndex = max(
0,
(
if (index > -1) {
(
items.take(index).indexOfLast { it?.fullPage == true }
)
} else {
-1
}
),
)
// Add a shifted page to the first place there isnt a full page
(fullPageBeforeIndex until items.size).forEach {
if (items[it]?.fullPage == false) {
items[it]?.shiftedPage = true
@@ -302,12 +292,15 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
// Step 4: Add blanks for chunking
var itemIndex = 0
while (itemIndex < items.size) {
items[itemIndex]?.isolatedPage = false
if (items[itemIndex]?.fullPage == true || items[itemIndex]?.shiftedPage == true) {
val currentItem = items[itemIndex]
currentItem?.isolatedPage = false
if (currentItem?.fullPage == true || currentItem?.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 (
currentItem.fullPage && 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
@@ -325,15 +318,14 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
// Step 5: chunk em
if (items.isNotEmpty()) {
subJoinedItems.addAll(
items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) },
)
subJoinedItems.addAll(items.chunked(2).map { Pair(it.first()!!, it.getOrNull(1)) })
}
otherItems.getOrNull(pagedIndex)?.let {
otherItems.getOrNull(pagedItems.indexOf(items))?.let {
subJoinedItems.add(Pair(it, null))
pagedIndex++
}
}
if (viewer is R2LPagerViewer) {
subJoinedItems.reverse()
}
@@ -347,42 +339,37 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
// we need to set the page back correctly
// We will however shift to the first page of the new chapter if the last page we were are
// on is not in the new chapter that has loaded
val newPage =
when {
(oldCurrent?.first as? ReaderPage)?.chapter != currentChapter &&
(oldCurrent?.first as? ChapterTransition)?.from != currentChapter -> subItems.find {
(it as? ReaderPage)?.chapter == currentChapter
}
useSecondPage -> (oldCurrent?.second ?: oldCurrent?.first)
else -> oldCurrent?.first ?: return
}
var index = joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
if (newPage is ChapterTransition && index == -1) {
val newerPage = if (newPage is ChapterTransition.Next) {
joinedItems.filter {
(it.first as? ReaderPage)?.chapter == newPage.to
}.minByOrNull { (it.first as? ReaderPage)?.index ?: Int.MAX_VALUE }?.first
} else {
joinedItems.filter {
(it.first as? ReaderPage)?.chapter == newPage.to
}.maxByOrNull { (it.first as? ReaderPage)?.index ?: Int.MIN_VALUE }?.first
}
index = joinedItems.indexOfFirst { it.first == newerPage || it.second == newerPage }
val newPage = when {
oldCurrent?.first is ReaderPage && (oldCurrent.first as ReaderPage).chapter != currentChapter &&
(oldCurrent.second as? ChapterTransition)?.from != currentChapter ->
subItems.find { it is ReaderPage && it.chapter == currentChapter }
useSecondPage -> oldCurrent?.second ?: oldCurrent?.first
else -> oldCurrent?.first ?: return
}
val index = when (newPage) {
is ChapterTransition -> {
val filteredPages = joinedItems.filter { it.first is ReaderPage && (it.first as ReaderPage).chapter == newPage.to }
val page = if (newPage is ChapterTransition.Next) {
filteredPages.minByOrNull { (it.first as ReaderPage).index }?.first
} else {
filteredPages.maxByOrNull { (it.first as ReaderPage).index }?.first
}
joinedItems.indexOfFirst { it.first == page || it.second == page }
}
else -> joinedItems.indexOfFirst { it.first == newPage || it.second == newPage }
}
viewer.pager.setCurrentItem(index, false)
}
fun splitDoublePages(current: ReaderPage) {
val oldCurrent = joinedItems.getOrNull(viewer.pager.currentItem)
setJoinedItems(
oldCurrent?.second == current ||
(current.index + 1) < (
(
oldCurrent?.second
?: oldCurrent?.first
) as? ReaderPage
)?.index ?: 0,
)
val oldSecondPage = oldCurrent?.second as? ReaderPage
val oldFirstPage = oldCurrent?.first as? ReaderPage
val oldPage = oldSecondPage ?: oldFirstPage
setJoinedItems(oldSecondPage == current || (current.index + 1) < (oldPage?.index ?: 0))
// The listener may be removed when we split a page, so the ui may not have updated properly
// This case usually happens when we load a new chapter and the first 2 pages need to split og
@@ -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
}
@@ -53,10 +53,15 @@ class WebtoonViewer(
*/
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.
@@ -68,11 +73,6 @@ class WebtoonViewer(
*/
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.
*/
@@ -86,6 +86,7 @@ class WebtoonViewer(
.threshold
init {
recycler.setItemViewCacheSize(RecyclerViewCacheSize)
recycler.isVisible = false // Don't let the recycler layout yet
recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
recycler.isFocusable = false
@@ -400,3 +401,5 @@ class WebtoonViewer(
)
}
}
private const val RecyclerViewCacheSize = 4
@@ -43,7 +43,7 @@ class StatsScreenModel(
// SY <--
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } }
private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
// SY -->
private val _allRead = MutableStateFlow(false)
@@ -10,7 +10,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.service.SourceManager
import uy.kohesive.injekt.Injekt
@@ -47,7 +47,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" }
}
}
}
@@ -4,7 +4,7 @@ import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toBitmap
import coil3.gif.ScaleDrawable
import coil3.size.ScaleDrawable
fun Drawable.getBitmapOrNull(): Bitmap? = when (this) {
is BitmapDrawable -> bitmap
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import okhttp3.OkHttpClient
import tachiyomi.domain.track.model.Track
import tachiyomi.i18n.MR
@@ -16,6 +18,7 @@ data class DummyTracker(
override val name: String,
override val supportsReadingDates: Boolean = false,
override val isLoggedIn: Boolean = false,
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
val valLogoColor: Int = Color.rgb(18, 25, 35),
val valLogo: Int = R.drawable.ic_tracker_anilist,
val valStatuses: List<Long> = (1L..6L).toList(),
+4 -4
View File
@@ -5,7 +5,7 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
ENGLISH("en"),
JAPANESE("ja"),
POLISH("pl"),
SERBO_CROATIAN("rs", "sh"),
SERBIAN("sh"),
DUTCH("nl"),
ITALIAN("it"),
RUSSIAN("ru"),
@@ -29,7 +29,7 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
MONGOLIAN("mn"),
TURKISH("tr"),
INDONESIAN("id"),
KOREAN("kr", "ko"),
KOREAN("ko"),
SPANISH_LATAM("es-la", "es-419"),
PERSIAN("fa"),
MALAY("ms"),
@@ -51,12 +51,12 @@ enum class MdLang(val lang: String, val extLang: String = lang) {
companion object {
fun fromIsoCode(isoCode: String): MdLang? =
values().firstOrNull {
entries.firstOrNull {
it.lang == isoCode
}
fun fromExt(extLang: String): MdLang? =
values().firstOrNull {
entries.firstOrNull {
it.extLang == extLang
}
}
@@ -31,6 +31,7 @@ private fun isHentaiTag(tag: String): Boolean {
tag.contains("nsfw", true) ||
tag.contains("erotica", true) ||
tag.contains("pornographic", true) ||
tag.contains("mature", true) ||
tag.contains("18+", true)
}
@@ -52,6 +53,8 @@ private fun isHentaiSource(source: String): Boolean {
source.contains("hbrowse", true) ||
source.contains("nhentai", true) ||
source.contains("erofus", true) ||
source.contains("luscious", true) ||
source.contains("doujins", true) ||
source.contains("multporn", true) ||
source.contains("vcp", true) ||
source.contains("vmp", true) ||
@@ -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),
)
}
@@ -10,7 +10,7 @@ import kotlinx.coroutines.runBlocking
object Migrator {
private var result: Deferred<Boolean>? = null
val scope = CoroutineScope(Dispatchers.Main + Job())
val scope = CoroutineScope(Dispatchers.IO + Job())
fun initialize(
old: Int,
@@ -1,9 +1,9 @@
package mihon.core.migration.migrations
import android.app.Application
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
@@ -12,7 +12,7 @@ class ChangeThemeModeToUppercaseMigration : Migration {
override val version: Float = 42f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val uiPreferences = migrationContext.get<UiPreferences>() ?: return@withIOContext false
if (uiPreferences.themeMode().isSet()) {
@@ -1,8 +1,8 @@
package mihon.core.migration.migrations
import android.app.Application
import android.content.Context
import androidx.core.content.edit
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
@@ -11,7 +11,7 @@ class ChangeTrackingQueueTypeMigration : Migration {
override val version: Float = 44f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
trackingQueuePref.all.forEach {
val (_, lastChapterRead) = it.value.toString().split(":")
@@ -1,6 +1,6 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import android.app.Application
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import logcat.LogPriority
import mihon.core.migration.Migration
@@ -13,7 +13,7 @@ class ClearBrokenPagePreviewCacheMigration : Migration {
override val version: Float = 58f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
val pagePreviewCache = migrationContext.get<PagePreviewCache>() ?: return@withIOContext false
pagePreviewCache.clear()
File(context.cacheDir, PagePreviewCache.PARAMETER_CACHE_DIRECTORY).listFiles()?.forEach {
@@ -1,6 +1,6 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import android.app.Application
import exh.log.xLogE
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
@@ -11,7 +11,7 @@ class DeleteOldEhFavoritesDatabaseMigration : Migration {
override val version: Float = 24f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
try {
sequenceOf(
"fav-sync",
@@ -1,7 +1,7 @@
package mihon.core.migration.migrations
import android.app.Application
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
@@ -11,7 +11,7 @@ class MoveCacheToDiskSettingMigration : Migration {
override val version: Float = 66f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
@@ -1,8 +1,8 @@
package mihon.core.migration.migrations
import android.app.Application
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
@@ -11,7 +11,7 @@ class MoveCatalogueCoverOnlyGridSettingMigration : Migration {
override val version: Float = 29f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val context = migrationContext.get<Application>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getString("pref_display_mode_catalogue", null) == "NO_TITLE_GRID") {
prefs.edit(commit = true) {

Some files were not shown because too many files have changed in this diff Show More