Compare commits

..

90 Commits

Author SHA1 Message Date
Mend Renovate 6cf7ef7bba Update dependency com.android.tools.build:gradle to v8.7.1 (#1326)
(cherry picked from commit 48166b9b52836f225273651b21fb02e7aba4197e)
2024-10-14 19:32:51 -04:00
AntsyLich 91d61a75e3 Make sure random library sort is at the bottom
(cherry picked from commit 2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
2024-10-14 19:32:35 -04:00
AntsyLich 95ae5211a7 Reorder reader menu overflow items
(cherry picked from commit 788235feeca241228eac0561339dd07b5ea0b77d)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2024-10-14 19:15:44 -04:00
AntsyLich 62afbf8ff3 Cleanup .gitignore files
(cherry picked from commit afa50029882655af8d5eea40aed7644fce4564d8)

# Conflicts:
#	.gitignore
#	app/.gitignore
2024-10-14 19:10:41 -04:00
Jobobby04 2ea8449eb7 Fix issue with not showing source names in merged manga sometimes 2024-10-14 16:49:49 -04:00
Fuad Hasan 697b0de226 Hide sync library option when sync is disabled (#1275)
* Hide sync library option when sync is disabled

- Add isSyncEnabled parameter to LibraryToolbar and LibraryRegularToolbar
- Pass isSyncEnabled from LibraryTab to LibraryToolbar
- Conditionally display sync library action based on isSyncEnabled
- Update LibraryContent to include isSyncEnabled parameter
- Adjust LibraryTab to handle sync-related functionality
- Remove direct dependency on SyncPreferences in LibraryToolbar

* Update LibraryScreenModel to react to sync service changes

- Replace static isSyncEnabled update with dynamic observation
- Use syncPreferences.syncService().changes() to update isSyncEnabled state
- Ensure isSyncEnabled is updated whenever the sync service changes

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>

* Fix sync service state update in LibraryScreenModel

- Resolve ambiguity in lambda parameters
- Correctly update isSyncEnabled state based on syncService value

* Optimize sync service state updates in LibraryScreenModel

- Add distinctUntilChanged() to filter out consecutive duplicate emissions
- Simplify mutableState.update lambda for better readability
- Remove redundant boolean comparison in isSyncEnabled assignment

---------

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
2024-10-14 16:35:49 -04:00
NGB-Was-Taken 41e523e074 Allow adding multiple tags separated by commas (#1282) 2024-10-14 16:34:58 -04:00
NGB-Was-Taken dee543c7c5 Respect the altTitlesInDesc preference (MD) (#1283)
* Respect the `altTitlesInDesc` preference (MD)

* Replace hardcoded "Alternative Titles" with localized string
2024-10-14 16:34:26 -04:00
Weblate (bot) 788d3797cb Translations update from Hosted Weblate (#1268)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Chrono Lux <amber_c001@protonmail.com>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Leandro Cândido <123888466+marshfellow42@users.noreply.github.com>
Co-authored-by: Miguel Angel Gaitan Ortega <gamersusa9@gmail.com>
Co-authored-by: Milihraim <kirill06678@gmail.com>
Co-authored-by: Phymos <phymosmusic@gmail.com>
Co-authored-by: Simon Saade <simonsaade005@gmail.com>
Co-authored-by: Tim Schneeberger <tim.schneeberger@outlook.de>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: akir45 <akkn0708@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: orkan gökçe alaz aşina <examplehuman@outlook.com>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2024-10-14 16:33:39 -04:00
Jobobby04 6464c00503 SpotlessApply 2024-10-14 16:23:36 -04:00
Cuong-Tran dc88ea8f63 honor downloadChapters (#1270) 2024-10-14 16:19:48 -04:00
renovate[bot] 95cbb35152 fix(deps): update dependency net.zetetic:sqlcipher-android to v4.6.1 (#1255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 16:16:02 -04:00
NGB-Was-Taken 558ce084c8 Show download state and progress on reader chapter list. (#1263)
* Show download state and progress on reader chapter list.

* Update download indicator on progress and status change.
2024-10-14 16:14:58 -04:00
Roshan Varughese 943555c0af Add option to backup non-library read entries (#1324)
Co-authored-by: jobobby04 <jobobby04@gmail.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit de36357da834bff4110dbb56dd7ce7aad04d3c7d)

# 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
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
2024-10-14 16:12:10 -04:00
AntsyLich 216bc2c57d Adjust expandable fab animation
Co-authored-by: p
(cherry picked from commit eb6092bd0cfa09694985a8bafdd8bbf2815190a1)
2024-10-14 15:28:40 -04:00
AntsyLich cde3002355 Refrain from running spotless on weblate files
Those are akin to generated files and are likely to not follow our formatting

(cherry picked from commit 32d2c2ac1bc224cbda2f09a4023d7d120ea0e954)

# Conflicts:
#	buildSrc/src/main/kotlin/mihon.code.lint.gradle.kts
2024-10-14 15:28:30 -04:00
brewkunz db907cf270 Fix EnhancedTracker not auto binding when adding manga to library (#1298)
(cherry picked from commit 3ed8a91c7b37ed89e6c4cab91daa79e6c846abe3)
2024-10-14 15:26:20 -04:00
Roshan Varughese a269802af9 Confirmation dialog when removing privately installed extensions (#1320)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 87db3f90dee8aa566038b46d0bdc975e715ab8f6)
2024-10-14 15:26:12 -04:00
Mend Renovate affab50a02 Update dependency me.zhanghai.android.libarchive:library to v1.1.3 (#1321)
(cherry picked from commit 0a4ad89b9902061e3e2c2d9f2eb71f6b33c5c01c)
2024-10-14 15:26:03 -04:00
Jack Hamilton 2f102db19d Added random library sort (#1317)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit a72db41bf1746db095fd27bbbce9765c06453581)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
#	domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt
2024-10-14 15:25:20 -04:00
Roshan Varughese 457e5f963b Add Quantity Badge to Upcoming Screen (#1250)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 6b2bba4e5495274552787adc20db390a89d783b6)
2024-10-14 15:20:37 -04:00
Roshan Varughese 2bd9a914c1 Add option to opt out of Analytics and Crashlytics (#1237)
(cherry picked from commit 7c7af72f8cb12decc06b76c36852dcc54696236d)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
#	app/src/standard/AndroidManifest.xml
2024-10-14 15:20:28 -04:00
AntsyLich f6d2d0bd48 Remove usage of deprecated accompanist SystemUiController
Co-authored-by: p
(cherry picked from commit 2ba3f0612c08c7021fed2f6d96cd538da2f34a13)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2024-10-14 15:05:44 -04:00
AntsyLich 91ae683b74 ChapterNavigator: dispatch page change only when needed
Co-authored-by: p
(cherry picked from commit f84d9a08b4af768b1e9920c43cc445c86f5427fc)
2024-10-14 14:59:06 -04:00
AntsyLich bccd1eff2b Bump compile sdk to 35
Co-authored-by: p
(cherry picked from commit 37419cdc26c2b5c4f8583fc2ba439b08fab42856)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
#	core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt
2024-10-14 14:58:56 -04:00
AntsyLich 9ed90eb6f2 Update resources exclusion rules
Co-authored-by: p
(cherry picked from commit 481cfedf08576cecfbb35616837bd8f627d8f959)
2024-10-14 14:48:08 -04:00
AntsyLich a246d897de Adjust distinct checker in WidgetManager and run on default dispatcher
Co-authored-by: p
(cherry picked from commit 9b8ab6acc25a5f99c9c5eebf9cc250975931c57c)
2024-10-14 14:47:59 -04:00
AntsyLich 4923ba0b54 Tweak Preference.collectAsState
Co-authored-by: p
(cherry picked from commit 3bddb5538528c19388e364d21e6a6c16487af759)
2024-10-14 14:47:34 -04:00
AntsyLich bd278b1878 Cleanup LibraryScreenModel LibraryMap.applySort and some more
(cherry picked from commit 2beb89d53163a6d288f8acdebe0f5d26fea8ab3e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
2024-10-14 14:47:20 -04:00
Mend Renovate ea0816a6c1 Update kotlin monorepo to v2.0.21 (#1314)
(cherry picked from commit 016f627fb0998dabcd6aea907b54365aa4e6a285)
2024-10-14 14:05:39 -04:00
brewkunz af3c7a0753 Retain remote last chapter read if it's higher than the local one for EnhancedTracker (#1301)
(cherry picked from commit 44aab7a2437ffdab354ee5ed82593fbaabb06a64)
2024-10-14 14:05:30 -04:00
Mend Renovate 4a9184bfc1 Update dependency io.mockk:mockk to v1.13.13 (#1313)
(cherry picked from commit a2dc88965b8b06cd40d65b75450e1ca4a1e08bd4)
2024-10-14 14:05:21 -04:00
AntsyLich 77d75de855 Update renovate configuration
- Remove package rule for "dev.chrisbanes.compose:compose-bom"
- Disable semantic commits

(cherry picked from commit aa998071a1f476a6078f19500bc58f7855c3f8ae)

# Conflicts:
#	.github/renovate.json5
2024-10-14 14:05:12 -04:00
Mend Renovate d7fbdb1b35 fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-rc01 (#1308)
(cherry picked from commit 8113b77f1e762629f31cbcc5b9163819c6384a8b)
2024-10-14 14:04:12 -04:00
Secozzi 6ad9eb098f Fix AniList ALSearchItem.status nullibility (#1297)
(cherry picked from commit 76e0aba70cc3a744e6ef4950a89ff6a498a54909)
2024-10-14 14:04:04 -04:00
Mend Renovate a6667bc91d fix(deps): update dependency androidx.compose:compose-bom to v2024.09.03 (#1288)
(cherry picked from commit f7fbc93833c6107791680412cc110336d0e4e717)
2024-10-14 14:03:56 -04:00
Mend Renovate 1e7b6d488c fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.2 (#1294)
(cherry picked from commit 85ee9c6686ee4f4ca5519297df7c4b5482cc26c2)
2024-10-14 14:03:48 -04:00
Mend Renovate d0ef7bcd54 fix(deps): update dependency androidx.profileinstaller:profileinstaller to v1.4.1 (#1289)
(cherry picked from commit c72c07f355a93f67d16166715dfdab88f2cc9201)
2024-10-14 14:03:40 -04:00
Mend Renovate e29e7c9169 fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.2 (#1287)
(cherry picked from commit 6984e0465babed7638481b1982de7415612f32e5)
2024-10-14 14:03:31 -04:00
Mend Renovate 4eb8dc35b9 fix(deps): update dependency com.google.firebase:firebase-bom to v33.4.0 (#1285)
(cherry picked from commit 3ca989eae8593a121fc22a617160f5d7439f937a)
2024-10-14 14:03:22 -04:00
Mend Renovate 1077820d59 fix(deps): update dependency com.android.tools.build:gradle to v8.7.0 (#1284)
(cherry picked from commit cca33481dd1466ae6a9919796229586fe0937523)
2024-10-14 14:02:39 -04:00
renovate[bot] ccf1f3b6ef chore(deps): update dependency gradle to v8.10.2 (#1254)
* chore(deps): update dependency gradle to v8.10.2

* Update binaries

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit f7c8f1801ea8c7af7542ab8e3dce035ada495c7c)
2024-10-14 14:02:30 -04:00
renovate[bot] 73d91a8537 fix(deps): update dependency androidx.compose:compose-bom to v2024.09.02 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 112b68b782d0f0ac027bf3d73ad28a8df0dc75b8)
2024-10-14 14:02:21 -04:00
renovate[bot] a141e63408 fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.1 (#1262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 2dd02b73d6059cef372e5d605efdafa7f60b47b0)
2024-10-14 14:02:15 -04:00
renovate[bot] 3e1c346a04 fix(deps): update dependency me.zhanghai.android.libarchive:library to v1.1.2 (#1255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit d04eeface97d64d921e9df23ffeba49d3eca2994)
2024-10-14 14:02:06 -04:00
renovate[bot] 6872dc449f fix(deps): update dependency androidx.profileinstaller:profileinstaller to v1.4.0 (#1242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 380787a31021d710a8a6619d4e0c1b01e3e47941)
2024-10-14 14:01:48 -04:00
renovate[bot] c672548491 fix(deps): update lifecycle.version to v2.8.6 (#1241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 418ba3026546b4785907c001a05006b609b490a3)
2024-10-14 14:01:40 -04:00
renovate[bot] 74abed9abd fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.1 (#1238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit b3867dd63c714333f58678f13b4cafc708cbd918)
2024-10-14 14:01:26 -04:00
renovate[bot] cb813908a6 fix(deps): update serialization.version to v1.7.3 (#1246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 6dd93d70cc5c7fa39157d069b41be5557256537e)
2024-10-14 14:01:18 -04:00
Roshan Varughese 049a395790 Change casing for Extention Repos String (#1248)
(cherry picked from commit 2276abbb2373b94535e99c2d72ce0f7f6a1d008a)
2024-10-14 14:01:10 -04:00
renovate[bot] bc79694eae fix(deps): update dependency com.android.tools.build:gradle to v8.6.1 (#1235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 0042cb6582f05d2a139b059bef81dc979e9a8ad6)
2024-10-14 14:00:41 -04:00
renovate[bot] 812f76b8f5 fix(deps): update dependency me.zhanghai.android.libarchive:library to v1.1.1 (#1229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1e570bc9654fb0382a8d5b37923c9700e49be696)
2024-10-14 14:00:34 -04:00
Roshan Varughese 4d8b5fc8a1 Re-enable fetching chapters list for entries with licenced status (#1230)
Enable Licensed

(cherry picked from commit 9cc7d42dd9676319559eddc7a4a264fca155e1ca)
2024-10-14 14:00:26 -04:00
MajorTanya a40c54e60c Fix Kitsu synopsis nullability (#1233)
This time, the Kitsu API docs are silent on whether this field (or
any other field) can be null/undefined/etc, but it can happen and
caused an error during search and update. This change just ensures the
attribute is nullable and is set to an empty String when it is null.

(cherry picked from commit f5c6d2e1a6896c031b8f4583375ee868f252822a)
2024-10-14 14:00:16 -04:00
Roshan Varughese e8ff402fff Fix WheelPicker Manual Input (#1209)
* Fix WheelPicker Manual Input

* Lambda

* inline

* Update WheelPicker.kt

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 339dc33f5833b224c01577da3da081deecdbbca2)
2024-10-14 14:00:02 -04:00
Cuong-Tran 0753ffe425 Fix: wrong calculation of nextUpdate when setting custom fetchInterval (#1206)
(cherry picked from commit 223af5508f6b56c7d7bf1c68d3c96a59a1ebc5d7)
2024-10-14 13:59:56 -04:00
renovate[bot] 03aa27fb6b fix(deps): update dependency androidx.compose:compose-bom to v2024.09.01 (#1214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit d42f776c5c5ddd8fade02bc7d0117a7c3e1054d5)
2024-10-14 13:59:48 -04:00
renovate[bot] 51b9004a2d fix(deps): update dependency com.google.firebase:firebase-bom to v33.3.0 (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 5c0dc3e05a8de6b4c75452ea10328935a0de0f37)
2024-10-14 13:59:42 -04:00
renovate[bot] 23285587a7 fix(deps): update dependency com.squareup.okio:okio to v3.9.1 (#1217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit bebf80dfaec037559af061950083289a0ae23b44)
2024-10-14 13:59:34 -04:00
renovate[bot] 5dcc02c44f fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.9.0 (#1222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1ff88dd9274db681ae0d76b39223389a1f758973)
2024-10-14 13:59:26 -04:00
renovate[bot] ecd38d9429 chore(deps): update dependency gradle to v8.10.1 (#1211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit fcb01b5bcf81e7c25ff820e99fcf10e867c3782f)
2024-10-14 13:59:15 -04:00
renovate[bot] 3b3e3f5d35 fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.3.8 (#1198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 844dae1a4d23b88318e0ea482b38df4e3f5f2be2)
2024-10-14 13:59:08 -04:00
NGB-Was-Taken 0d66d03f56 Show toast for app restart when User-Agent is changed (#1204)
(cherry picked from commit c8ad6cdf31a14bce9a525cfc2a0616e8ac51d7c3)
2024-10-14 13:58:53 -04:00
AntsyLich a68bb60126 Bump NDK version (#1203)
(cherry picked from commit fbcc48fefc7ed050f6416a8684816730bcb5f8a8)

# Conflicts:
#	buildSrc/src/main/kotlin/mihon/buildlogic/AndroidConfig.kt
2024-10-14 13:58:45 -04:00
AntsyLich 1e9f7612f0 Reduce ChapterNavigator horizontal padding on small ui (#1202)
Co-authored-by: p
(cherry picked from commit 6f422745ba6db135514ac4959216923932565760)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/reader/components/ChapterNavigator.kt
2024-10-14 13:58:16 -04:00
AntsyLich 51229ca511 Use TextFieldState in BasicTextField where applicable (#1201)
Co-authored-by: p
(cherry picked from commit bec549cc444328c8b46594c67a84d25dcc1aca6f)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt
2024-10-14 13:56:14 -04:00
Jobobby04 b98e198e15 SpotlessApply 2024-10-14 13:54:54 -04:00
bapeey 3cac63ed91 Ignore "intent://" urls on webview (#1193)
ignore intent urls

(cherry picked from commit b56a97bb8eaf336c75e69e7fb32b87431d991bb3)
2024-10-14 13:50:55 -04:00
renovate[bot] 6bb2bc03f3 fix(deps): update dependency androidx.activity:activity-compose to v1.9.2 (#1189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 52036e5664cbcf552de706adee6e0b4b972fe1c3)
2024-10-14 13:50:48 -04:00
renovate[bot] da3823daed fix(deps): update dependency com.google.accompanist:accompanist-systemuicontroller to v0.36.0 (#1192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 29a74509a4af475694551808e317df96ea1146ad)
2024-10-14 13:50:39 -04:00
renovate[bot] 3edb03de32 fix(deps): update lifecycle.version to v2.8.5 (#1190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 0e956cbb518e0e0827c1e7dfde8427cb8660a9fb)
2024-10-14 13:50:31 -04:00
Tran M. Cuong 3408ef635d Fix disappearance items when fast scrolling (#1035)
* Don't use animateItem's fade-in/fade-out in FastScrollLazyColumn

* Move to extension function

Avoid using animateItemPlacement name since it's shadowed by compose-bom's deprecated one

(cherry picked from commit 913ff22132390a59a13c463645ce954c7cbc5c6b)
2024-10-14 13:50:10 -04:00
renovate[bot] 365cd0b14d fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2024.05.00-alpha03 (#843)
* fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2024.05.00-alpha03

* 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 777a071f4a2f32efc3447d118afd8b48006b3919)
2024-10-14 13:39:16 -04:00
AntsyLich 96439afce4 Bump compose version
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit e473c7f09fc009161145aca94bd70027f042b0bf)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
#	gradle/compose.versions.toml
2024-10-14 13:38:38 -04:00
AntsyLich c1c615000b Switch to stable compose
(cherry picked from commit 2baffa62cade1abd978d5fd03151b47fc87fd31e)

# Conflicts:
#	gradle/compose.versions.toml
2024-10-14 13:37:01 -04:00
AntsyLich 58df8b79fb Rename LocalesConfigPlugin file to LocalesConfigTask
(cherry picked from commit 70c1a842b207d8faf0d87635674667d190669fd1)
2024-10-14 13:01:00 -04:00
MajorTanya f524763854 Fix Kitsu ratingTwenty being typed as String (#1191)
The API docs and the responses type `ratingTwenty` as a "number" (Int
in Kotlin, it's divided by 2 for a .5 step scale 0-10). It's nullable
because an entry without a user rating returns `null` in that field.

(cherry picked from commit 001249a89dd4824a3df5661733062662c0ab44bd)
2024-10-14 13:00:48 -04:00
renovate[bot] 402f5e6bad fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.2 (#1188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit c4d2fffb12c83c76cf48a85cbc9d7d754a4da39c)
2024-10-14 13:00:35 -04:00
AntsyLich 0d13c6187c Fix mishap in 02af9b1acf9f590d29560bc3fc90d206e8e6e1af
(cherry picked from commit f22767d863a0fa001f93f24092cd5ade87350502)
2024-10-14 13:00:13 -04:00
AntsyLich 1853a86a73 Remove more unnecessary permissions from Firebase dependency
(cherry picked from commit 02af9b1acf9f590d29560bc3fc90d206e8e6e1af)
2024-10-14 13:00:07 -04:00
AntsyLich 77e6e06cfa Add crashlytics to standard builds
(cherry picked from commit 3c611b95fb79e5ac972019b76c7b24f46a3087fd)

# Conflicts:
#	app/build.gradle.kts
2024-10-14 12:59:57 -04:00
AntsyLich 21440a0290 Migrate some classpaths to gradle plugins
(cherry picked from commit fc1c804bfda1d76c0399bbb6214e75b3def951cc)

# Conflicts:
#	app/build.gradle.kts
#	build.gradle.kts
#	i18n/build.gradle.kts
2024-10-14 12:32:35 -04:00
Roshan Varughese d6ffef15e1 Option to update trackers when chapter marked as read (#1177)
* Track when marked as read

* Add dismiss to snack bar

* i18n & ignore decimal chapters

* Detekt would have caught that 🤣

* `Ok` > `Yes`

* Dont prompt if untracked or current > new

* Move to MangaScreenModel

* Suggestions

Co-Authored-By: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* Review 2

* toggleAllSelections first

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit abfb72c89c008973db866bf4b696b699db155574)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2024-10-14 12:27:14 -04:00
Smol Ame 3cc250e122 Enable 'Split Tall Images' by default (#1185)
(cherry picked from commit 9c1905ede750f0229fad1a01431058b1cc9fb32d)
2024-10-14 12:26:23 -04:00
MajorTanya 051c559840 Use DTOs to parse tracking API responses (#1103)
* Migrate tracking APIs to DTOs

Changes the handling of tracker API responses to be parsed to DTOs
instead of doing so "manually" by use of `jsonPrimitive`s and/or
`Json.decodeFromString` invocations.

This greatly simplifies the API response handling.

Renamed constants to SCREAMING_SNAKE_CASE.

Largely tried to name the DTOs in a uniform pattern, with the
tracker's (short) name at the beginning of file and data class names
(ALOAuth instead of OAuth, etc).

With these changes, no area of the code base should be using
`jsonPrimitive` and/or `Json.decodeFromString` anymore.

* Fix wrong types in KitsuAlgoliaSearchItem

This API returns start and end dates as Long and the score as Double.

Kitsu's docs claim they're strings (and they are, when requesting
manga details from Kitsu directly) but the Algolia search results
return Longs and Double, respectively.

* Apply review changes

- Renamed `BangumiX` classes to `BGMX` classes.
- Renamed `toXStatus` and `toXScore` to `toApiStatus` and `toApiScore`

* Handle migration from detekt to spotless

Removed Suppressions added for detekt.

Specifically removed:
- `SwallowedException` where an exception ends as a default value
- `MagicNumber`
- `CyclomaticComplexMethod`
- `TooGenericExceptionThrown`

Also ran spotlessApply which changed SMAddMangaResponse

* Fix Kitsu failing to add series

The `included` attribute seems to only appear when the user already
has the entry in their Kitsu list.

Since both `data` and `included` are required for `firstToTrack`, a
guard clause has been added before all its calls.

* Fix empty Bangumi error when entry doesn't exist

Previously, the non-null assertion (!!) would cause a
NullPointerException and a Toast with
"Bangumi error: " (no message) when the user had removed their list
entry from Bangumi through other means like the website.

Now it will show "Bangumi error: Could not find manga".

This is analogous to the error shown by Kitsu under these
circumstances.

* Fix Shikimori ignoring missing remote entry

The user would see no indication that Shikimori could not properly
refresh the track from the remote. This change causes the error Toast
notification to pop up with the following message
"Shikimori error: Could not find manga".

This is analogous to Kitsu and Bangumi.

* Remove usage of let where not needed

These particular occurrences weren't needed because properties are
directly accessible to further act upon. This neatly simplifies these
clauses.

* Remove missed let

(cherry picked from commit 9f99f038f341e325c4f56372a5ce950cf9f7cd6d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistInterceptor.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt
2024-10-14 12:26:09 -04:00
AntsyLich 3972d7fe4b spotlessApply my beloved
(cherry picked from commit 6c6ea84509cc1bd859c880bebbc69067a241b358)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
2024-10-14 12:19:01 -04:00
AntsyLich 44fd9f3564 Add stable marker to Manga data class
Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 4ee31bfea5b6908e7131e2c46e4cb46155005abf)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	domain/build.gradle.kts
2024-10-14 12:17:18 -04:00
AntsyLich f36906df45 Collect MangaScreen state with lifecycle
Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit 03eb756ecba0692d88d3a76254afc4c157fa225b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2024-10-14 12:07:03 -04:00
AntsyLich efbaf1a4ca PagerPageHolder: lazy init loading indicator
Co-authored-by: ivan <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
2024-10-14 12:05:13 -04:00
Jobobby04 2f8efe0526 Switch Injekt to Koin-Injekt bridge and implement InjektRegistrar bridge 2024-10-14 11:59:02 -04:00
225 changed files with 4159 additions and 2055 deletions
+2 -3
View File
@@ -1,8 +1,7 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:base"],
"config:base"
],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"includePaths": [".github/workflows/*", "gradle/sy.versions.toml"], "includePaths": [".github/workflows/*", "gradle/sy.versions.toml"],
"semanticCommits": "disabled"
} }
+15 -20
View File
@@ -1,26 +1,21 @@
# Build files
.gradle .gradle
.kotlin .kotlin
/local.properties build
/.idea/workspace.xml
.DS_Store # IDE files
*.iml
.idea/* .idea/*
!.idea/icon.png !.idea/icon.png
*iml
*.iml
/mainframer
/.mainframer
# Built files
*/build
/build
*.apk
app/**/output.json
# Unnecessary file
*.swp
TODO.md
CHANGELOG.md
/captures /captures
build.sh
# Configuration files
local.properties
# macOS specific files
.DS_Store
# SY ignores
google-services.json
/app/src/main/assets/client_secrets.json /app/src/main/assets/client_secrets.json
*.apk
-4
View File
@@ -1,4 +0,0 @@
/build
*iml
*.iml
google-services.json
+20 -10
View File
@@ -6,17 +6,18 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("mihon.android.application") id("mihon.android.application")
id("mihon.android.application.compose") id("mihon.android.application.compose")
id("com.mikepenz.aboutlibraries.plugin")
kotlin("plugin.parcelize") kotlin("plugin.parcelize")
kotlin("plugin.serialization") kotlin("plugin.serialization")
// id("com.github.zellius.shortcut-helper") // id("com.github.zellius.shortcut-helper")
alias(libs.plugins.aboutLibraries)
id("com.github.ben-manes.versions") id("com.github.ben-manes.versions")
} }
if (gradle.startParameter.taskRequests.toString().contains("Standard")) { if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply<com.google.gms.googleservices.GoogleServicesPlugin>() pluginManager.apply {
// Firebase Crashlytics apply(libs.plugins.google.services.get().pluginId)
apply(plugin = "com.google.firebase.crashlytics") apply(libs.plugins.firebase.crashlytics.get().pluginId)
}
} }
// shortcutHelper.setFilePath("./shortcuts.xml") // shortcutHelper.setFilePath("./shortcuts.xml")
@@ -106,13 +107,16 @@ android {
packaging { packaging {
resources.excludes.addAll( resources.excludes.addAll(
listOf( listOf(
"kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES", "META-INF/DEPENDENCIES",
"LICENSE.txt", "LICENSE.txt",
"META-INF/LICENSE", "META-INF/LICENSE",
"META-INF/LICENSE.txt", "META-INF/**/LICENSE.txt",
"META-INF/*.properties",
"META-INF/**/*.properties",
"META-INF/README.md", "META-INF/README.md",
"META-INF/NOTICE", "META-INF/NOTICE",
"META-INF/*.kotlin_module", "META-INF/*.version",
), ),
) )
} }
@@ -161,7 +165,6 @@ dependencies {
debugImplementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview) implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.interpolator) implementation(androidx.interpolator)
@@ -247,7 +250,9 @@ dependencies {
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics // Crash reports/analytics
// "standardImplementation"(libs.firebase.analytics) // "standardImplementation"(platform(libs.firebase.bom))
// "standardImplementation"(libs.firebase.analytics)
// "standardImplementation"(libs.firebase.crashlytics)
// Shizuku // Shizuku
implementation(libs.bundles.shizuku) implementation(libs.bundles.shizuku)
@@ -266,8 +271,9 @@ dependencies {
implementation(sylibs.simularity) implementation(sylibs.simularity)
// Firebase (EH) // Firebase (EH)
implementation(sylibs.firebase.analytics) implementation(platform(libs.firebase.bom))
implementation(sylibs.firebase.crashlytics.ktx) implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
// Better logging (EH) // Better logging (EH)
implementation(sylibs.xlog) implementation(sylibs.xlog)
@@ -279,6 +285,10 @@ dependencies {
// Google drive // Google drive
implementation(sylibs.google.api.services.drive) implementation(sylibs.google.api.services.drive)
implementation(sylibs.google.api.client.oauth) implementation(sylibs.google.api.client.oauth)
// Koin
implementation(sylibs.koin.core)
implementation(sylibs.koin.android)
} }
androidComponents { androidComponents {
@@ -0,0 +1,9 @@
package mihon.core.firebase
import android.content.Context
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) = Unit
}
+21 -1
View File
@@ -34,11 +34,23 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Remove permission from Firebase dependency --> <!-- Remove unnecessary permissions from Firebase dependency -->
<uses-permission
android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"
tools:node="remove" />
<uses-permission <uses-permission
android:name="com.google.android.gms.permission.AD_ID" android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" /> tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
tools:node="remove" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"
@@ -256,6 +268,14 @@
android:name="android.webkit.WebView.MetricsOptOut" android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" /> android:value="true" />
<!-- Disable for manual opt-in -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable advertising ID collection for Firebase --> <!-- Disable advertising ID collection for Firebase -->
<meta-data <meta-data
android:name="google_analytics_adid_collection_enabled" android:name="google_analytics_adid_collection_enabled"
@@ -5,7 +5,7 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?, generator: (before: T?, after: T?) -> R?,
): List<R> { ): List<R> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val newList = mutableListOf<R>() val newList = mutableListOf<R>()
@@ -19,6 +19,24 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList return newList
} }
/**
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
*/
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
generator: (before: T?, after: T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in size downTo 0) {
val after = getOrNull(i)
after?.let(newList::add)
val before = getOrNull(i - 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList.asReversed()
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) add(value)
@@ -23,6 +23,9 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import mihon.data.repository.ExtensionRepoRepositoryImpl import mihon.data.repository.ExtensionRepoRepositoryImpl
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
@@ -91,11 +94,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.repository.TrackRepository import tachiyomi.domain.track.repository.TrackRepository
import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.repository.UpdatesRepository import tachiyomi.domain.updates.repository.UpdatesRepository
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class DomainModule : InjektModule { class DomainModule : InjektModule {
@@ -14,6 +14,9 @@ import eu.kanade.domain.source.interactor.GetSourceCategories
import eu.kanade.domain.source.interactor.RenameSourceCategory import eu.kanade.domain.source.interactor.RenameSourceCategory
import eu.kanade.domain.source.interactor.SetSourceCategories import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.tachiyomi.di.InjektModule
import eu.kanade.tachiyomi.di.addFactory
import eu.kanade.tachiyomi.di.addSingletonFactory
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import exh.search.SearchEngine import exh.search.SearchEngine
import tachiyomi.data.manga.CustomMangaRepositoryImpl import tachiyomi.data.manga.CustomMangaRepositoryImpl
@@ -42,7 +45,7 @@ import tachiyomi.domain.manga.interactor.GetMergedManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById import tachiyomi.domain.manga.interactor.GetMergedMangaById
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
import tachiyomi.domain.manga.interactor.GetMergedReferencesById import tachiyomi.domain.manga.interactor.GetMergedReferencesById
import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibrary import tachiyomi.domain.manga.interactor.GetReadMangaNotInLibraryView
import tachiyomi.domain.manga.interactor.GetSearchMetadata import tachiyomi.domain.manga.interactor.GetSearchMetadata
import tachiyomi.domain.manga.interactor.GetSearchTags import tachiyomi.domain.manga.interactor.GetSearchTags
import tachiyomi.domain.manga.interactor.GetSearchTitles import tachiyomi.domain.manga.interactor.GetSearchTitles
@@ -71,11 +74,7 @@ import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.repository.FeedSavedSearchRepository import tachiyomi.domain.source.repository.FeedSavedSearchRepository
import tachiyomi.domain.source.repository.SavedSearchRepository import tachiyomi.domain.source.repository.SavedSearchRepository
import tachiyomi.domain.track.interactor.IsTrackUnfollowed import tachiyomi.domain.track.interactor.IsTrackUnfollowed
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class SYDomainModule : InjektModule { class SYDomainModule : InjektModule {
@@ -102,7 +101,7 @@ class SYDomainModule : InjektModule {
addFactory { GetPagePreviews(get(), get()) } addFactory { GetPagePreviews(get(), get()) }
addFactory { SearchEngine() } addFactory { SearchEngine() }
addFactory { IsTrackUnfollowed() } addFactory { IsTrackUnfollowed() }
addFactory { GetReadMangaNotInLibrary(get()) } addFactory { GetReadMangaNotInLibraryView(get()) }
// Required for [MetadataSource] // Required for [MetadataSource]
addFactory<MetadataSource.GetMangaId> { GetManga(get()) } addFactory<MetadataSource.GetMangaId> { GetManga(get()) }
@@ -5,6 +5,7 @@ import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority import logcat.LogPriority
@@ -14,17 +15,16 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZoneOffset import java.time.ZoneOffset
class AddTracks( class AddTracks(
private val getTracks: GetTracks,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val trackerManager: TrackerManager,
) { ) {
// TODO: update all trackers based on common data // TODO: update all trackers based on common data
@@ -79,7 +79,7 @@ class AddTracks(
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext { withIOContext {
getTracks.await(manga.id) trackerManager.loggedInTrackers()
.filterIsInstance<EnhancedTracker>() .filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) } .filter { it.accept(source) }
.forEach { service -> .forEach { service ->
@@ -87,11 +87,11 @@ class AddTracks(
service.match(manga)?.let { track -> service.match(manga)?.let { track ->
track.manga_id = manga.id track.manga_id = manga.id
(service as Tracker).bind(track) (service as Tracker).bind(track)
insertTrack.await(track.toDomainTrack()!!) insertTrack.await(track.toDomainTrack(idRequired = false)!!)
syncChapterProgressWithTrack.await( syncChapterProgressWithTrack.await(
manga.id, manga.id,
track.toDomainTrack()!!, track.toDomainTrack(idRequired = false)!!,
service, service,
) )
} }
@@ -10,6 +10,7 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import kotlin.math.max
class SyncChapterProgressWithTrack( class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
@@ -36,7 +37,8 @@ class SyncChapterProgressWithTrack(
// only take into account continuous reading // only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble()) val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble())
val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead)
try { try {
tracker.update(updatedTrack.toDbTrack()) tracker.update(updatedTrack.toDbTrack())
@@ -198,7 +198,7 @@ private fun ExtensionDetails(
key = { it.source.id }, key = { it.source.id },
) { source -> ) { source ->
SourceSwitchPreference( SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = source, source = source,
onClickSourcePreferences = onClickSourcePreferences, onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource, onClickSource = onClickSource,
@@ -58,7 +58,7 @@ private fun ExtensionFilterContent(
) { ) {
items(state.languages) { language -> items(state.languages) { language ->
SwitchPreferenceWidget( SwitchPreferenceWidget(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
title = LocaleHelper.getSourceDisplayName(language, context), title = LocaleHelper.getSourceDisplayName(language, context),
checked = language in state.enabledLanguages, checked = language in state.enabledLanguages,
onCheckedChanged = { onClickLang(language) }, onCheckedChanged = { onClickLang(language) },
@@ -48,6 +48,7 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
@@ -91,7 +92,7 @@ fun ExtensionScreen(
PullRefresh( PullRefresh(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = { !state.isLoading }, enabled = !state.isLoading,
) { ) {
when { when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
@@ -188,14 +189,14 @@ private fun ExtensionContent(
} }
ExtensionHeader( ExtensionHeader(
textRes = header.textRes, textRes = header.textRes,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
action = action, action = action,
) )
} }
is ExtensionUiModel.Header.Text -> { is ExtensionUiModel.Header.Text -> {
ExtensionHeader( ExtensionHeader(
text = header.text, text = header.text,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
) )
} }
} }
@@ -213,7 +214,7 @@ private fun ExtensionContent(
}, },
) { item -> ) { item ->
ExtensionItem( ExtensionItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
item = item, item = item,
onClickItem = { onClickItem = {
when (it) { when (it) {
@@ -92,7 +92,7 @@ fun FeedScreen(
refreshing = true refreshing = true
onRefresh() onRefresh()
}, },
enabled = { !state.isLoadingItems }, enabled = !state.isLoadingItems,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
contentPadding = contentPadding + topSmallPaddingValues, contentPadding = contentPadding + topSmallPaddingValues,
@@ -103,7 +103,6 @@ fun FeedScreen(
key = { it.feed.id }, key = { it.feed.id },
) { item -> ) { item ->
GlobalSearchResultItem( GlobalSearchResultItem(
modifier = Modifier.animateItemPlacement(),
title = item.title, title = item.title,
subtitle = item.subtitle, subtitle = item.subtitle,
onLongClick = { onLongClick = {
@@ -116,6 +115,7 @@ fun FeedScreen(
onClickSource(item.source) onClickSource(item.source)
} }
}, },
modifier = Modifier.animateItem(),
) { ) {
FeedItem( FeedItem(
item = item, item = item,
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
@@ -80,6 +81,7 @@ internal fun GlobalSearchContent(
} ?: source.name, } ?: source.name,
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
onClick = { onClickSource(source) }, onClick = { onClickSource(source) },
modifier = Modifier.animateItem(),
) { ) {
when (result) { when (result) {
SearchItemResult.Loading -> { SearchItemResult.Loading -> {
@@ -144,7 +144,7 @@ private fun MigrateSourceList(
key = { (source, _) -> "migrate-${source.id}" }, key = { (source, _) -> "migrate-${source.id}" },
) { (source, count) -> ) { (source, count) ->
MigrateSourceItem( MigrateSourceItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = source, source = source,
count = count, count = count,
onClickItem = { onClickItem(source) }, onClickItem = { onClickItem(source) },
@@ -28,6 +28,7 @@ import eu.kanade.presentation.browse.components.MigrationItem
import eu.kanade.presentation.browse.components.MigrationItemResult import eu.kanade.presentation.browse.components.MigrationItemResult
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -95,7 +96,7 @@ fun MigrationListScreen(
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.animateItemPlacement() .animateItemFastScroll()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.height(IntrinsicSize.Min), .height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -15,6 +15,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.components.AppBarTitle import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.animateItemFastScroll
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.FeedSavedSearch import tachiyomi.domain.source.model.FeedSavedSearch
@@ -153,7 +154,7 @@ fun SourceFeedList(
key = { it.id }, key = { it.id },
) { item -> ) { item ->
GlobalSearchResultItem( GlobalSearchResultItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
title = item.title, title = item.title,
subtitle = null, subtitle = null,
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) { onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {
@@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
@@ -79,7 +80,7 @@ private fun SourcesFilterContent(
contentType = "source-filter-header", contentType = "source-filter-header",
) { ) {
SourcesFilterHeader( SourcesFilterHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
language = language, language = language,
enabled = enabled, enabled = enabled,
onClickItem = onClickLanguage, onClickItem = onClickLanguage,
@@ -95,7 +96,7 @@ private fun SourcesFilterContent(
sources.none { it.id.toString() in state.disabledSources } sources.none { it.id.toString() in state.disabledSources }
} }
SourcesFilterToggle( SourcesFilterToggle(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
isEnabled = toggleEnabled, isEnabled = toggleEnabled,
onClickItem = { onClickItem = {
onClickSources(!toggleEnabled, sources) onClickSources(!toggleEnabled, sources)
@@ -109,7 +110,7 @@ private fun SourcesFilterContent(
contentType = { "source-filter-item" }, contentType = { "source-filter-item" },
) { source -> ) { source ->
SourcesFilterItem( SourcesFilterItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
source = source, source = source,
enabled = "${source.id}" !in state.disabledSources, enabled = "${source.id}" !in state.disabledSources,
onClickItem = onClickSource, onClickItem = onClickSource,
@@ -81,7 +81,7 @@ fun SourcesScreen(
when (model) { when (model) {
is SourceUiModel.Header -> { is SourceUiModel.Header -> {
SourceHeader( SourceHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
language = model.language, language = model.language,
// SY --> // SY -->
isCategory = model.isCategory, isCategory = model.isCategory,
@@ -89,7 +89,7 @@ fun SourcesScreen(
) )
} }
is SourceUiModel.Item -> SourceItem( is SourceUiModel.Item -> SourceItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
source = model.source, source = model.source,
// SY --> // SY -->
showLatest = state.showLatest, showLatest = state.showLatest,
@@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) { return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext { withIOContext {
value = try { value = try {
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
val appResources = context.packageManager.getResourcesForApplication(appInfo) val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success( Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!! appResources.getDrawableForDensity(appInfo.icon, density, null)!!
@@ -30,9 +30,6 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun GlobalSearchResultItem( fun GlobalSearchResultItem(
// SY -->
modifier: Modifier = Modifier,
// SY <--
title: String, title: String,
// SY --> // SY -->
subtitle: String?, subtitle: String?,
@@ -41,9 +38,10 @@ fun GlobalSearchResultItem(
// SY --> // SY -->
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
// SY <-- // SY <--
modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Column(modifier) { Column(modifier = modifier) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -107,7 +107,7 @@ private fun CategoryContent(
key = { _, category -> "category-${category.id}" }, key = { _, category -> "category-${category.id}" },
) { index, category -> ) { index, category ->
CategoryListItem( CategoryListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
category = category, category = category,
canMoveUp = index != 0, canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex, canMoveDown = index != categories.lastIndex,
@@ -10,8 +10,7 @@ import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrollingUp
@Composable @Composable
fun CategoryFloatingActionButton( fun CategoryFloatingActionButton(
@@ -23,7 +22,7 @@ fun CategoryFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) }, text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) }, icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
onClick = onCreate, onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(), expanded = lazyListState.shouldExpandFAB(),
modifier = modifier, modifier = modifier,
) )
} }
@@ -26,7 +26,7 @@ fun BiometricTimesContent(
) { ) {
items(timeRanges, key = { it.formattedString }) { timeRange -> items(timeRanges, key = { it.formattedString }) { timeRange ->
BiometricTimesListItem( BiometricTimesListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
timeRange = timeRange, timeRange = timeRange,
onDelete = { onClickDelete(timeRange) }, onDelete = { onClickDelete(timeRange) },
) )
@@ -27,7 +27,7 @@ fun SortTagContent(
) { ) {
itemsIndexed(tags, key = { _, tag -> tag }) { index, tag -> itemsIndexed(tags, key = { _, tag -> tag }) { index, tag ->
SortTagListItem( SortTagListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
tag = tag, tag = tag,
canMoveUp = index != 0, canMoveUp = index != 0,
canMoveDown = index != tags.lastIndex, canMoveDown = index != tags.lastIndex,
@@ -26,7 +26,7 @@ fun SourceCategoryContent(
) { ) {
items(categories, key = { it }) { category -> items(categories, key = { it }) { category ->
SourceCategoryListItem( SourceCategoryListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
category = category, category = category,
onRename = { onClickRename(category) }, onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) }, onDelete = { onClickDelete(category) },
@@ -179,7 +179,7 @@ fun AppBarTitle(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee( modifier = Modifier.basicMarquee(
delayMillis = 2_000, repeatDelayMillis = 2_000,
), ),
) )
} }
@@ -18,6 +18,7 @@ import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -114,14 +115,14 @@ private fun HistoryScreenContent(
when (item) { when (item) {
is HistoryUiModel.Header -> { is HistoryUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
text = relativeDateText(item.date), text = relativeDateText(item.date),
) )
} }
is HistoryUiModel.Item -> { is HistoryUiModel.Item -> {
val value = item.item val value = item.item
HistoryItem( HistoryItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
history = value, history = value,
onClickCover = { onClickCover(value) }, onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) }, onClickResume = { onClickResume(value) },
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -35,6 +37,7 @@ import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.BaseSortItem
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.IconItem import tachiyomi.presentation.core.components.IconItem
@@ -197,39 +200,58 @@ private fun ColumnScope.SortPage(
globalSortMode.type globalSortMode.type
} }
val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) { val sortDescending = if (screenModel.grouping == LibraryGroup.BY_DEFAULT) {
category.sort.isAscending !category.sort.isAscending
} else { } else {
globalSortMode.isAscending !globalSortMode.isAscending
}.not() }
val hasSortTags by remember { val hasSortTags by remember {
screenModel.libraryPreferences.sortTagsForLibrary().changes() screenModel.libraryPreferences.sortTagsForLibrary().changes()
.map { it.isNotEmpty() } .map { it.isNotEmpty() }
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty()) }.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
// SY <-- // SY <--
val trackerSortOption = if (trackers.isEmpty()) { val options = remember(trackers.isEmpty()/* SY --> */, hasSortTags/* SY <-- */) {
emptyList() val trackerMeanPair = if (trackers.isNotEmpty()) {
} else { MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) } else {
} null
}
listOfNotNull(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount,
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
// SY --> // SY -->
if (hasSortTags) { val tagSortPair = if (hasSortTags) {
SYMR.strings.tag_sorting to LibrarySort.Type.TagList SYMR.strings.tag_sorting to LibrarySort.Type.TagList
} else { } else {
null null
}, }
// SY <-- // SY <--
).plus(trackerSortOption).map { (titleRes, mode) -> listOfNotNull(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount,
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
trackerMeanPair,
// SY -->
tagSortPair,
// SY <--
MR.strings.action_sort_random to LibrarySort.Type.Random,
)
}
options.map { (titleRes, mode) ->
if (mode == LibrarySort.Type.Random) {
BaseSortItem(
label = stringResource(titleRes),
icon = Icons.Default.Refresh
.takeIf { sortingMode == LibrarySort.Type.Random },
onClick = {
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
},
)
return@map
}
SortItem( SortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode }, sortDescending = sortDescending.takeIf { sortingMode == mode },
@@ -241,7 +263,11 @@ private fun ColumnScope.SortPage(
} else { } else {
LibrarySort.Direction.Descending LibrarySort.Direction.Descending
} }
else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending else -> if (sortDescending) {
LibrarySort.Direction.Descending
} else {
LibrarySort.Direction.Ascending
}
} }
screenModel.setSort(category, mode, direction) screenModel.setSort(category, mode, direction)
}, },
@@ -95,7 +95,7 @@ fun LibraryContent(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = { notSelectionMode }, enabled = notSelectionMode,
) { ) {
LibraryPager( LibraryPager(
state = pagerState, state = pagerState,
@@ -41,6 +41,7 @@ fun LibraryToolbar(
onClickSyncNow: () -> Unit, onClickSyncNow: () -> Unit,
// SY --> // SY -->
onClickSyncExh: (() -> Unit)?, onClickSyncExh: (() -> Unit)?,
isSyncEnabled: Boolean,
// SY <-- // SY <--
searchQuery: String?, searchQuery: String?,
onSearchQueryChange: (String?) -> Unit, onSearchQueryChange: (String?) -> Unit,
@@ -64,6 +65,7 @@ fun LibraryToolbar(
onClickSyncNow = onClickSyncNow, onClickSyncNow = onClickSyncNow,
// SY --> // SY -->
onClickSyncExh = onClickSyncExh, onClickSyncExh = onClickSyncExh,
isSyncEnabled = isSyncEnabled,
// SY <-- // SY <--
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
@@ -82,6 +84,7 @@ private fun LibraryRegularToolbar(
onClickSyncNow: () -> Unit, onClickSyncNow: () -> Unit,
// SY --> // SY -->
onClickSyncExh: (() -> Unit)?, onClickSyncExh: (() -> Unit)?,
isSyncEnabled: Boolean,
// SY <-- // SY <--
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
) { ) {
@@ -128,10 +131,6 @@ private fun LibraryRegularToolbar(
title = stringResource(MR.strings.action_open_random_manga), title = stringResource(MR.strings.action_open_random_manga),
onClick = onClickOpenRandomManga, onClick = onClickOpenRandomManga,
), ),
AppBar.OverflowAction(
title = stringResource(SYMR.strings.sync_library),
onClick = onClickSyncNow,
),
).builder().apply { ).builder().apply {
// SY --> // SY -->
if (onClickSyncExh != null) { if (onClickSyncExh != null) {
@@ -142,6 +141,14 @@ private fun LibraryRegularToolbar(
), ),
) )
} }
if (isSyncEnabled) {
add(
AppBar.OverflowAction(
title = stringResource(SYMR.strings.sync_library),
onClick = onClickSyncNow,
),
)
}
// SY <-- // SY <--
}.build(), }.build(),
) )
@@ -103,8 +103,7 @@ import tachiyomi.presentation.core.components.material.ExtendedFloatingActionBut
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.shouldExpandFAB
import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@@ -432,7 +431,7 @@ private fun MangaScreenSmallImpl(
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), expanded = chapterListState.shouldExpandFAB(),
) )
} }
}, },
@@ -442,7 +441,7 @@ private fun MangaScreenSmallImpl(
PullRefresh( PullRefresh(
refreshing = state.isRefreshingData, refreshing = state.isRefreshingData,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = { !isAnySelected }, enabled = !isAnySelected,
indicatorPadding = PaddingValues(top = topPadding), indicatorPadding = PaddingValues(top = topPadding),
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
@@ -467,13 +466,9 @@ private fun MangaScreenSmallImpl(
MangaInfoBox( MangaInfoBox(
isTabletUi = false, isTabletUi = false,
appBarPadding = topPadding, appBarPadding = topPadding,
title = state.manga.title, manga = state.manga,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) }, sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource }, isStubSource = remember { state.source is StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked, onCoverClick = onCoverClicked,
doSearch = onSearch, doSearch = onSearch,
) )
@@ -757,7 +752,7 @@ fun MangaScreenLargeImpl(
}, },
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading, onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), expanded = chapterListState.shouldExpandFAB(),
) )
} }
}, },
@@ -765,7 +760,7 @@ fun MangaScreenLargeImpl(
PullRefresh( PullRefresh(
refreshing = state.isRefreshingData, refreshing = state.isRefreshingData,
onRefresh = onRefresh, onRefresh = onRefresh,
enabled = { !isAnySelected }, enabled = !isAnySelected,
indicatorPadding = PaddingValues( indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection), start = insetPadding.calculateStartPadding(layoutDirection),
top = with(density) { topBarHeight.toDp() }, top = with(density) { topBarHeight.toDp() },
@@ -786,13 +781,9 @@ fun MangaScreenLargeImpl(
MangaInfoBox( MangaInfoBox(
isTabletUi = true, isTabletUi = true,
appBarPadding = contentPadding.calculateTopPadding(), appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title, manga = state.manga,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) }, sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is StubSource }, isStubSource = remember { state.source is StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked, onCoverClick = onCoverClicked,
doSearch = onSearch, doSearch = onSearch,
) )
@@ -57,7 +57,7 @@ import tachiyomi.presentation.core.util.clickableNoIndication
@Composable @Composable
fun MangaCoverDialog( fun MangaCoverDialog(
coverDataProvider: () -> Manga, manga: Manga,
isCustomCover: Boolean, isCustomCover: Boolean,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
onShareClick: () -> Unit, onShareClick: () -> Unit,
@@ -167,7 +167,7 @@ fun MangaCoverDialog(
}, },
update = { view -> update = { view ->
val request = ImageRequest.Builder(view.context) val request = ImageRequest.Builder(view.context)
.data(coverDataProvider()) .data(manga)
.size(Size.ORIGINAL) .size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.target { image -> .target { image ->
@@ -75,6 +75,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -99,13 +101,9 @@ private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTIL
fun MangaInfoBox( fun MangaInfoBox(
isTabletUi: Boolean, isTabletUi: Boolean,
appBarPadding: Dp, appBarPadding: Dp,
title: String, manga: Manga,
author: String?,
artist: String?,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
coverDataProvider: () -> Manga,
status: Long,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -117,7 +115,10 @@ fun MangaInfoBox(
MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.background,
) )
AsyncImage( AsyncImage(
model = coverDataProvider(), model = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@@ -137,28 +138,20 @@ fun MangaInfoBox(
if (!isTabletUi) { if (!isTabletUi) {
MangaAndSourceTitlesSmall( MangaAndSourceTitlesSmall(
appBarPadding = appBarPadding, appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider, manga = manga,
onCoverClick = onCoverClick,
title = title,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName, sourceName = sourceName,
isStubSource = isStubSource, isStubSource = isStubSource,
onCoverClick = onCoverClick,
doSearch = doSearch,
) )
} else { } else {
MangaAndSourceTitlesLarge( MangaAndSourceTitlesLarge(
appBarPadding = appBarPadding, appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider, manga = manga,
onCoverClick = onCoverClick,
title = title,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName, sourceName = sourceName,
isStubSource = isStubSource, isStubSource = isStubSource,
onCoverClick = onCoverClick,
doSearch = doSearch,
) )
} }
} }
@@ -377,15 +370,11 @@ fun ExpandableMangaDescription(
@Composable @Composable
private fun MangaAndSourceTitlesLarge( private fun MangaAndSourceTitlesLarge(
appBarPadding: Dp, appBarPadding: Dp,
coverDataProvider: () -> Manga, manga: Manga,
onCoverClick: () -> Unit,
title: String,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
status: Long,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -395,19 +384,22 @@ private fun MangaAndSourceTitlesLarge(
) { ) {
MangaCover.Book( MangaCover.Book(
modifier = Modifier.fillMaxWidth(0.65f), modifier = Modifier.fillMaxWidth(0.65f),
data = coverDataProvider(), data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
contentDescription = stringResource(MR.strings.manga_cover), contentDescription = stringResource(MR.strings.manga_cover),
onClick = onCoverClick, onClick = onCoverClick,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
MangaContentInfo( MangaContentInfo(
title = title, title = manga.title,
doSearch = doSearch, author = manga.author,
author = author, artist = manga.artist,
artist = artist, status = manga.status,
status = status,
sourceName = sourceName, sourceName = sourceName,
isStubSource = isStubSource, isStubSource = isStubSource,
doSearch = doSearch,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }
@@ -416,15 +408,11 @@ private fun MangaAndSourceTitlesLarge(
@Composable @Composable
private fun MangaAndSourceTitlesSmall( private fun MangaAndSourceTitlesSmall(
appBarPadding: Dp, appBarPadding: Dp,
coverDataProvider: () -> Manga, manga: Manga,
onCoverClick: () -> Unit,
title: String,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
artist: String?,
status: Long,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -437,7 +425,10 @@ private fun MangaAndSourceTitlesSmall(
modifier = Modifier modifier = Modifier
.sizeIn(maxWidth = 100.dp) .sizeIn(maxWidth = 100.dp)
.align(Alignment.Top), .align(Alignment.Top),
data = coverDataProvider(), data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
contentDescription = stringResource(MR.strings.manga_cover), contentDescription = stringResource(MR.strings.manga_cover),
onClick = onCoverClick, onClick = onCoverClick,
) )
@@ -445,13 +436,13 @@ private fun MangaAndSourceTitlesSmall(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
MangaContentInfo( MangaContentInfo(
title = title, title = manga.title,
doSearch = doSearch, author = manga.author,
author = author, artist = manga.artist,
artist = artist, status = manga.status,
status = status,
sourceName = sourceName, sourceName = sourceName,
isStubSource = isStubSource, isStubSource = isStubSource,
doSearch = doSearch,
) )
} }
} }
@@ -460,12 +451,12 @@ private fun MangaAndSourceTitlesSmall(
@Composable @Composable
private fun ColumnScope.MangaContentInfo( private fun ColumnScope.MangaContentInfo(
title: String, title: String,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?, author: String?,
artist: String?, artist: String?,
status: Long, status: Long,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
doSearch: (query: String, global: Boolean) -> Unit,
textAlign: TextAlign? = LocalTextStyle.current.textAlign, textAlign: TextAlign? = LocalTextStyle.current.textAlign,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -141,7 +141,7 @@ fun TagsChip(
border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(), border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(),
borderM3: BorderStroke? = SuggestionChipDefaultsM3.suggestionChipBorder(enabled = true), borderM3: BorderStroke? = SuggestionChipDefaultsM3.suggestionChipBorder(enabled = true),
) { ) {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
if (onClick != null) { if (onClick != null) {
SuggestionChip( SuggestionChip(
modifier = modifier, modifier = modifier,
@@ -32,8 +32,6 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun ScanlatorFilterDialog( fun ScanlatorFilterDialog(
@@ -97,8 +95,8 @@ fun ScanlatorFilterDialog(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
} }
}, },
properties = DialogProperties( properties = DialogProperties(
@@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -28,19 +30,24 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep { internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false) private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false) private var batteryGranted by mutableStateOf(false)
@@ -73,7 +80,7 @@ internal class PermissionStep : OnboardingStep {
} }
Column { Column {
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_install_apps), title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted, granted = installGranted,
@@ -89,7 +96,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume // no-op. resulting checks is being done on resume
}, },
) )
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_notifications), title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted, granted = notificationGranted,
@@ -97,7 +104,7 @@ internal class PermissionStep : OnboardingStep {
) )
} }
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted, granted = batteryGranted,
@@ -109,6 +116,29 @@ internal class PermissionStep : OnboardingStep {
context.startActivity(intent) context.startActivity(intent)
}, },
) )
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
val crashlyticsPref = privacyPreferences.crashlytics()
val crashlytics by crashlyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
granted = crashlytics,
onToggleChange = crashlyticsPref::set,
)
val analyticsPref = privacyPreferences.analytics()
val analytics by analyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
granted = analytics,
onToggleChange = analyticsPref::set,
)
} }
} }
@@ -127,7 +157,7 @@ internal class PermissionStep : OnboardingStep {
} }
@Composable @Composable
private fun PermissionItem( private fun PermissionCheckbox(
title: String, title: String,
subtitle: String, subtitle: String,
granted: Boolean, granted: Boolean,
@@ -157,4 +187,26 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
) )
} }
@Composable
private fun PermissionSwitch(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onToggleChange: (Boolean) -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
Switch(
checked = granted,
onCheckedChange = onToggleChange,
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
} }
@@ -299,6 +299,7 @@ object SettingsAdvancedScreen : SearchableSettings {
try { try {
// OkHttp checks for valid values internally // OkHttp checks for valid values internally
Headers.Builder().add("User-Agent", it) Headers.Builder().add("User-Agent", it)
context.toast(MR.strings.requires_app_restart)
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
context.toast(MR.strings.error_user_agent_string_invalid) context.toast(MR.strings.error_user_agent_string_invalid)
return@EditTextPreference false return@EditTextPreference false
@@ -13,8 +13,10 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -28,11 +30,8 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -43,7 +42,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -88,9 +86,7 @@ class SettingsSearchScreen : Screen() {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { val textFieldState = rememberTextFieldState()
mutableStateOf(TextFieldValue())
}
Scaffold( Scaffold(
topBar = { topBar = {
Column { Column {
@@ -105,20 +101,19 @@ class SettingsSearchScreen : Screen() {
}, },
title = { title = {
BasicTextField( BasicTextField(
value = textFieldValue, state = textFieldState,
onValueChange = { textFieldValue = it },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester) .focusRequester(focusRequester)
.runOnEnterKeyPressed(action = focusManager::clearFocus), .runOnEnterKeyPressed(action = focusManager::clearFocus),
textStyle = MaterialTheme.typography.bodyLarge textStyle = MaterialTheme.typography.bodyLarge
.copy(color = MaterialTheme.colorScheme.onSurface), .copy(color = MaterialTheme.colorScheme.onSurface),
singleLine = true, lineLimits = TextFieldLineLimits.SingleLine,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }), onKeyboardAction = { focusManager.clearFocus() },
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { decorator = {
if (textFieldValue.text.isEmpty()) { if (textFieldState.text.isEmpty()) {
Text( Text(
text = stringResource(MR.strings.action_search_settings), text = stringResource(MR.strings.action_search_settings),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -130,8 +125,8 @@ class SettingsSearchScreen : Screen() {
) )
}, },
actions = { actions = {
if (textFieldValue.text.isNotEmpty()) { if (textFieldState.text.isNotEmpty()) {
IconButton(onClick = { textFieldValue = TextFieldValue() }) { IconButton(onClick = { textFieldState.clearText() }) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = null, contentDescription = null,
@@ -146,7 +141,7 @@ class SettingsSearchScreen : Screen() {
}, },
) { contentPadding -> ) { contentPadding ->
SearchResult( SearchResult(
searchKey = textFieldValue.text, searchKey = textFieldState.text.toString(),
listState = listState, listState = listState,
contentPadding = contentPadding, contentPadding = contentPadding,
) { result -> ) { result ->
@@ -45,6 +45,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
@@ -70,10 +71,20 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() } val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() } val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return listOf(
getSecurityGroup(securityPreferences),
getFirebaseGroup(privacyPreferences),
)
}
@Composable
private fun getSecurityGroup(
securityPreferences: SecurityPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator() val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState() val useAuth by useAuthPref.collectAsState()
@@ -81,129 +92,132 @@ object SettingsSecurityScreen : SearchableSettings {
val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState() val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState()
val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState() val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState()
return listOf( return Preference.PreferenceGroup(
Preference.PreferenceItem.SwitchPreference( title = stringResource(MR.strings.pref_security),
pref = useAuthPref, preferenceItems = persistentListOf(
title = stringResource(MR.strings.lock_with_biometrics), Preference.PreferenceItem.SwitchPreference(
enabled = authSupported, pref = useAuthPref,
onValueChanged = { title = stringResource(MR.strings.lock_with_biometrics),
(context as FragmentActivity).authenticate( enabled = authSupported,
title = context.stringResource(MR.strings.lock_with_biometrics), onValueChanged = {
) (context as FragmentActivity).authenticate(
}, title = context.stringResource(MR.strings.lock_with_biometrics),
), )
Preference.PreferenceItem.ListPreference( },
pref = securityPreferences.lockAppAfter(), ),
title = stringResource(MR.strings.lock_when_idle), Preference.PreferenceItem.ListPreference(
enabled = authSupported && useAuth, pref = securityPreferences.lockAppAfter(),
entries = LockAfterValues title = stringResource(MR.strings.lock_when_idle),
.associateWith { enabled = authSupported && useAuth,
when (it) { entries = LockAfterValues
-1 -> stringResource(MR.strings.lock_never) .associateWith {
0 -> stringResource(MR.strings.lock_always) when (it) {
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) -1 -> stringResource(MR.strings.lock_never)
0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
}
} }
.toImmutableMap(),
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
)
},
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.passwordProtectDownloads(),
title = stringResource(SYMR.strings.password_protect_downloads),
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.encryptionType(),
title = stringResource(SYMR.strings.encryption_type),
entries = SecurityPreferences.EncryptionType.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
enabled = passwordProtectDownloads,
),
kotlin.run {
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
PasswordDialog(
onDismissRequest = { dialogOpen = false },
onReturnPassword = { password ->
dialogOpen = false
CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", "")))
},
)
} }
.toImmutableMap(), Preference.PreferenceItem.TextPreference(
onValueChanged = { title = stringResource(SYMR.strings.set_cbz_zip_password),
(context as FragmentActivity).authenticate( onClick = {
title = context.stringResource(MR.strings.lock_when_idle), dialogOpen = true
)
},
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
// SY -->
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.passwordProtectDownloads(),
title = stringResource(SYMR.strings.password_protect_downloads),
subtitle = stringResource(SYMR.strings.password_protect_downloads_summary),
enabled = isCbzPasswordSet,
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.encryptionType(),
title = stringResource(SYMR.strings.encryption_type),
entries = SecurityPreferences.EncryptionType.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
enabled = passwordProtectDownloads,
),
kotlin.run {
var dialogOpen by remember { mutableStateOf(false) }
if (dialogOpen) {
PasswordDialog(
onDismissRequest = { dialogOpen = false },
onReturnPassword = { password ->
dialogOpen = false
CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", "")))
}, },
) )
}
Preference.PreferenceItem.TextPreference(
title = stringResource(SYMR.strings.set_cbz_zip_password),
onClick = {
dialogOpen = true
},
)
},
Preference.PreferenceItem.TextPreference(
title = stringResource(SYMR.strings.delete_cbz_archive_password),
onClick = {
CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set("")
}, },
enabled = isCbzPasswordSet,
),
kotlin.run {
val navigator = LocalNavigator.currentOrThrow
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(SYMR.strings.action_edit_biometric_lock_times), title = stringResource(SYMR.strings.delete_cbz_archive_password),
subtitle = pluralStringResource(
SYMR.plurals.num_lock_times,
count.size,
count.size,
),
onClick = { onClick = {
navigator.push(BiometricTimesScreen()) CbzCrypto.deleteKeyCbz()
securityPreferences.cbzPassword().set("")
}, },
enabled = useAuth, enabled = isCbzPasswordSet,
) ),
}, kotlin.run {
kotlin.run { val navigator = LocalNavigator.currentOrThrow
val selection by securityPreferences.authenticatorDays().collectAsState() val count by securityPreferences.authenticatorTimeRanges().collectAsState()
var dialogOpen by remember { mutableStateOf(false) } Preference.PreferenceItem.TextPreference(
if (dialogOpen) { title = stringResource(SYMR.strings.action_edit_biometric_lock_times),
SetLockedDaysDialog( subtitle = pluralStringResource(
onDismissRequest = { dialogOpen = false }, SYMR.plurals.num_lock_times,
initialSelection = selection, count.size,
onDaysSelected = { count.size,
dialogOpen = false ),
securityPreferences.authenticatorDays().set(it) onClick = {
navigator.push(BiometricTimesScreen())
}, },
enabled = useAuth,
) )
} },
Preference.PreferenceItem.TextPreference( kotlin.run {
title = stringResource(SYMR.strings.biometric_lock_days), val selection by securityPreferences.authenticatorDays().collectAsState()
subtitle = stringResource(SYMR.strings.biometric_lock_days_summary), var dialogOpen by remember { mutableStateOf(false) }
onClick = { dialogOpen = true }, if (dialogOpen) {
enabled = useAuth, SetLockedDaysDialog(
) onDismissRequest = { dialogOpen = false },
}, initialSelection = selection,
// SY <-- onDaysSelected = {
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), dialogOpen = false
securityPreferences.authenticatorDays().set(it)
},
)
}
Preference.PreferenceItem.TextPreference(
title = stringResource(SYMR.strings.biometric_lock_days),
subtitle = stringResource(SYMR.strings.biometric_lock_days_summary),
onClick = { dialogOpen = true },
enabled = useAuth,
)
},
// SY <--
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
),
) )
} }
@@ -361,6 +375,28 @@ object SettingsSecurityScreen : SearchableSettings {
) )
} }
// SY <-- // SY <--
@Composable
private fun getFirebaseGroup(
privacyPreferences: PrivacyPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
),
)
}
} }
private val LockAfterValues = persistentListOf( private val LockAfterValues = persistentListOf(
@@ -46,7 +46,7 @@ fun ExtensionReposContent(
repos.forEach { repos.forEach {
item { item {
ExtensionRepoListItem( ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
repo = it, repo = it,
onOpenWebsite = { onOpenWebsite(it) }, onOpenWebsite = { onOpenWebsite(it) },
onDelete = { onClickDelete(it.baseUrl) }, onDelete = { onClickDelete(it.baseUrl) },
@@ -29,32 +29,24 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import tachiyomi.core.common.preference.InMemoryPreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
@Composable @Composable
internal fun AppThemePreferenceWidget( internal fun AppThemePreferenceWidget(
@@ -257,18 +249,18 @@ fun AppThemePreviewItem(
} }
} }
@PreviewLightDark // @PreviewLightDark
@Composable // @Composable
private fun AppThemesListPreview() { // private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) } // var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore())) // Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
TachiyomiTheme(appTheme = appTheme) { // TachiyomiTheme(appTheme = appTheme) {
Surface { // Surface {
AppThemesList( // AppThemesList(
currentTheme = appTheme, // currentTheme = appTheme,
amoled = false, // amoled = false,
onItemClick = { appTheme = it }, // onItemClick = { appTheme = it },
) // )
} // }
} // }
} // }
@@ -26,8 +26,6 @@ import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun <T> ListPreferenceWidget( fun <T> ListPreferenceWidget(
@@ -69,8 +67,8 @@ fun <T> ListPreferenceWidget(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
} }
}, },
confirmButton = { confirmButton = {
@@ -30,8 +30,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
private enum class State { private enum class State {
CHECKED, CHECKED,
@@ -117,16 +115,8 @@ fun <T> TriStateListDialog(
} }
} }
if (!listState.isScrolledToStart()) { if (listState.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
HorizontalDivider( if (listState.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
modifier = Modifier.align(Alignment.TopCenter),
)
}
if (!listState.isScrolledToEnd()) {
HorizontalDivider(
modifier = Modifier.align(Alignment.BottomCenter),
)
}
} }
} }
}, },
@@ -8,11 +8,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
@@ -20,8 +22,13 @@ import eu.kanade.tachiyomi.util.lang.toRelativeString
import exh.metadata.MetadataUtil import exh.metadata.MetadataUtil
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
@@ -39,6 +46,8 @@ fun ChapterListDialog(
val manga by screenModel.mangaFlow.collectAsState() val manga by screenModel.mangaFlow.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val state = rememberLazyListState(chapters.indexOfFirst { it.isCurrent }.coerceAtLeast(0)) val state = rememberLazyListState(chapters.indexOfFirst { it.isCurrent }.coerceAtLeast(0))
val downloadManager: DownloadManager = remember { Injekt.get() }
val downloadQueueState by downloadManager.queueState.collectAsState()
AdaptiveSheet( AdaptiveSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -52,6 +61,28 @@ fun ChapterListDialog(
items = chapters, items = chapters,
key = { "chapter-${it.chapter.id}" }, key = { "chapter-${it.chapter.id}" },
) { chapterItem -> ) { chapterItem ->
val activeDownload = downloadQueueState.find { it.chapter.id == chapterItem.chapter.id }
val progress = activeDownload?.let {
downloadManager.progressFlow()
.filter { it.chapter.id == chapterItem.chapter.id }
.map { it.progress }
.collectAsState(0).value
} ?: 0
val downloaded = if (chapterItem.manga.isLocal()) {
true
} else {
downloadManager.isChapterDownloaded(
chapterItem.chapter.name,
chapterItem.chapter.scanlator,
chapterItem.manga.ogTitle,
chapterItem.manga.source,
)
}
val downloadState = when {
activeDownload != null -> activeDownload.status
downloaded -> Download.State.DOWNLOADED
else -> Download.State.NOT_DOWNLOADED
}
MangaChapterListItem( MangaChapterListItem(
title = chapterItem.chapter.name, title = chapterItem.chapter.name,
date = chapterItem.chapter.dateUpload date = chapterItem.chapter.dateUpload
@@ -76,8 +107,8 @@ fun ChapterListDialog(
bookmark = chapterItem.chapter.bookmark, bookmark = chapterItem.chapter.bookmark,
selected = false, selected = false,
downloadIndicatorEnabled = false, downloadIndicatorEnabled = false,
downloadStateProvider = { Download.State.NOT_DOWNLOADED }, downloadStateProvider = { downloadState },
downloadProgressProvider = { 0 }, downloadProgressProvider = { progress },
chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark, chapterSwipeStartAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark, chapterSwipeEndAction = LibraryPreferences.ChapterSwipeAction.ToggleBookmark,
onLongClick = { /*TODO*/ }, onLongClick = { /*TODO*/ },
@@ -46,6 +46,7 @@ fun BottomReaderBar(
doublePages: Boolean, doublePages: Boolean,
onClickChapterList: () -> Unit, onClickChapterList: () -> Unit,
onClickWebView: (() -> Unit)?, onClickWebView: (() -> Unit)?,
onClickBrowser: (() -> Unit)?,
onClickShare: (() -> Unit)?, onClickShare: (() -> Unit)?,
onClickPageLayout: () -> Unit, onClickPageLayout: () -> Unit,
onClickShiftPage: () -> Unit, onClickShiftPage: () -> Unit,
@@ -78,6 +79,15 @@ fun BottomReaderBar(
} }
} }
if (ReaderBottomButton.Browser.isIn(enabledButtons) && onClickBrowser != null) {
IconButton(onClick = onClickBrowser) {
Icon(
imageVector = Icons.Outlined.Public,
contentDescription = stringResource(MR.strings.action_open_in_browser),
)
}
}
if (ReaderBottomButton.Share.isIn(enabledButtons) && onClickShare != null) { if (ReaderBottomButton.Share.isIn(enabledButtons) && onClickShare != null) {
IconButton(onClick = onClickShare) { IconButton(onClick = onClickShare) {
Icon( Icon(
@@ -74,6 +74,7 @@ fun ReaderAppBars(
// bookmarked: Boolean, // bookmarked: Boolean,
// onToggleBookmarked: () -> Unit, // onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?, onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?,
onShare: (() -> Unit)?, onShare: (() -> Unit)?,
viewer: Viewer?, viewer: Viewer?,
@@ -308,6 +309,7 @@ fun ReaderAppBars(
doublePages = doublePages, doublePages = doublePages,
onClickChapterList = onClickChapterList, onClickChapterList = onClickChapterList,
onClickWebView = onOpenInWebView, onClickWebView = onOpenInWebView,
onClickBrowser = onOpenInBrowser,
onClickShare = onShare, onClickShare = onShare,
onClickPageLayout = onClickPageLayout, onClickPageLayout = onClickPageLayout,
onClickShiftPage = onClickShiftPage, onClickShiftPage = onClickShiftPage,
@@ -25,7 +25,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -36,9 +38,11 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.isTabletUi import eu.kanade.presentation.util.isTabletUi
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -75,7 +79,7 @@ fun ChapterNavigator(
} }
// SY <-- // SY <--
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()
val horizontalPadding = if (isTabletUi) 24.dp else 16.dp val horizontalPadding = if (isTabletUi) 24.dp else 8.dp
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
@@ -138,7 +142,10 @@ fun ChapterNavigator(
valueRange = 1f..totalPages.toFloat(), valueRange = 1f..totalPages.toFloat(),
steps = totalPages - 2, steps = totalPages - 2,
onValueChange = { onValueChange = {
onSliderValueChange(it.roundToInt() - 1) val new = it.roundToInt() - 1
if (new != currentPage) {
onSliderValueChange(new)
}
}, },
interactionSource = interactionSource, interactionSource = interactionSource,
) )
@@ -180,7 +187,7 @@ fun ChapterNavigatorVert(
onSliderValueChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
) { ) {
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()
val verticalPadding = if (isTabletUi) 24.dp else 16.dp val verticalPadding = if (isTabletUi) 24.dp else 8.dp
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
@@ -280,3 +287,25 @@ fun ChapterNavigatorVert(
} }
} }
} }
@Preview
@Composable
private fun ChapterNavigatorPreview() {
var currentPage by remember { mutableIntStateOf(1) }
TachiyomiPreviewTheme {
ChapterNavigator(
isRtl = false,
onNextChapter = {},
enabledNext = true,
onPreviousChapter = {},
enabledPrevious = true,
currentPage = currentPage,
totalPages = 10,
onSliderValueChange = { currentPage = it },
// SY -->
currentPageText = "1",
isVerticalSlider = false,
// SY <--
)
}
}
@@ -43,8 +43,6 @@ import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
@Composable @Composable
fun TrackStatusSelector( fun TrackStatusSelector(
@@ -86,8 +84,8 @@ fun TrackStatusSelector(
} }
} }
} }
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) if (state.canScrollBackward) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) if (state.canScrollForward) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}, },
onConfirm = onConfirm, onConfirm = onConfirm,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
@@ -59,7 +61,6 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
@@ -84,8 +85,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun TrackerSearch( fun TrackerSearch(
query: TextFieldValue, state: TextFieldState,
onQueryChange: (TextFieldValue) -> Unit,
onDispatchQuery: () -> Unit, onDispatchQuery: () -> Unit,
queryResult: Result<List<TrackSearch>>?, queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?, selected: TrackSearch?,
@@ -115,20 +115,19 @@ fun TrackerSearch(
}, },
title = { title = {
BasicTextField( BasicTextField(
value = query, state = state,
onValueChange = onQueryChange,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester) .focusRequester(focusRequester)
.runOnEnterKeyPressed(action = dispatchQueryAndClearFocus), .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus),
textStyle = MaterialTheme.typography.bodyLarge textStyle = MaterialTheme.typography.bodyLarge
.copy(color = MaterialTheme.colorScheme.onSurface), .copy(color = MaterialTheme.colorScheme.onSurface),
singleLine = true, lineLimits = TextFieldLineLimits.SingleLine,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { dispatchQueryAndClearFocus() }), onKeyboardAction = { dispatchQueryAndClearFocus() },
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { decorator = {
if (query.text.isEmpty()) { if (state.text.isEmpty()) {
Text( Text(
text = stringResource(MR.strings.action_search_hint), text = stringResource(MR.strings.action_search_hint),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -140,10 +139,10 @@ fun TrackerSearch(
) )
}, },
actions = { actions = {
if (query.text.isNotEmpty()) { if (state.text.isNotEmpty()) {
IconButton( IconButton(
onClick = { onClick = {
onQueryChange(TextFieldValue()) state.clearText()
focusRequester.requestFocus() focusRequester.requestFocus()
}, },
) { ) {
@@ -1,7 +1,7 @@
package eu.kanade.presentation.track package eu.kanade.presentation.track
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@@ -13,8 +13,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
private val fullPageWithSecondSelected = @Composable { private val fullPageWithSecondSelected = @Composable {
val items = someTrackSearches().take(30).toList() val items = someTrackSearches().take(30).toList()
TrackerSearch( TrackerSearch(
query = TextFieldValue(text = "search text"), state = TextFieldState(initialText = "search text"),
onQueryChange = {},
onDispatchQuery = {}, onDispatchQuery = {},
queryResult = Result.success(items), queryResult = Result.success(items),
selected = items[1], selected = items[1],
@@ -25,8 +24,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
} }
private val fullPageWithoutSelected = @Composable { private val fullPageWithoutSelected = @Composable {
TrackerSearch( TrackerSearch(
query = TextFieldValue(text = ""), state = TextFieldState(),
onQueryChange = {},
onDispatchQuery = {}, onDispatchQuery = {},
queryResult = Result.success(someTrackSearches().take(30).toList()), queryResult = Result.success(someTrackSearches().take(30).toList()),
selected = null, selected = null,
@@ -37,8 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
} }
private val loading = @Composable { private val loading = @Composable {
TrackerSearch( TrackerSearch(
query = TextFieldValue(), state = TextFieldState(),
onQueryChange = {},
onDispatchQuery = {}, onDispatchQuery = {},
queryResult = null, queryResult = null,
selected = null, selected = null,
@@ -107,7 +107,7 @@ fun UpdateScreen(
isRefreshing = false isRefreshing = false
} }
}, },
enabled = { !state.selectionMode }, enabled = !state.selectionMode,
indicatorPadding = contentPadding, indicatorPadding = contentPadding,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
@@ -37,6 +37,7 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText import eu.kanade.presentation.manga.components.DotSeparatorText
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem import eu.kanade.tachiyomi.ui.updates.UpdatesItem
@@ -54,7 +55,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
item(key = "updates-lastUpdated") { item(key = "updates-lastUpdated") {
Box( Box(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small),
) { ) {
Text( Text(
@@ -94,14 +95,14 @@ internal fun LazyListScope.updatesUiItems(
when (item) { when (item) {
is UpdatesUiModel.Header -> { is UpdatesUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
text = relativeDateText(item.date), text = relativeDateText(item.date),
) )
} }
is UpdatesUiModel.Item -> { is UpdatesUiModel.Item -> {
val updatesItem = item.item val updatesItem = item.item
UpdatesUiItem( UpdatesUiItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemFastScroll(),
update = updatesItem.update, update = updatesItem.update,
selected = updatesItem.selected, selected = updatesItem.selected,
readProgress = updatesItem.update.lastPageRead readProgress = updatesItem.update.lastPageRead
@@ -2,7 +2,6 @@ package eu.kanade.presentation.util
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.data.source.NoResultsException import tachiyomi.data.source.NoResultsException
@@ -25,7 +24,6 @@ val Throwable.formattedMessage: String
is NoResultsException -> return stringResource(MR.strings.no_results_found) is NoResultsException -> return stringResource(MR.strings.no_results_found)
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error) is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
is LicensedMangaChaptersException -> return stringResource(MR.strings.licensed_manga_chapters_error)
} }
return when (val className = this::class.simpleName) { return when (val className = this::class.simpleName) {
"Exception", "IOException" -> message ?: className "Exception", "IOException" -> message ?: className
@@ -0,0 +1,8 @@
package eu.kanade.presentation.util
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.ui.Modifier
// https://issuetracker.google.com/352584409
context(LazyItemScope)
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
@@ -60,7 +60,10 @@ interface AssistContentScreen {
} }
@Composable @Composable
fun DefaultNavigatorScreenTransition(navigator: Navigator) { fun DefaultNavigatorScreenTransition(
navigator: Navigator,
modifier: Modifier = Modifier,
) {
val slideDistance = rememberSlideDistance() val slideDistance = rememberSlideDistance()
ScreenTransition( ScreenTransition(
navigator = navigator, navigator = navigator,
@@ -70,6 +73,7 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) {
slideDistance = slideDistance, slideDistance = slideDistance,
) )
}, },
modifier = modifier,
) )
} }
@@ -9,9 +9,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable @Composable
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean { fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
@@ -104,6 +104,11 @@ fun WebViewScreenContent(
return false return false
} }
// Ignore intents urls
if (it.url.toString().startsWith("intent://")) {
return true
}
// Continue with request, but with custom headers // Continue with request, but with custom headers
view?.loadUrl(it.url.toString(), headers) view?.loadUrl(it.url.toString(), headers)
} }
+10 -9
View File
@@ -28,14 +28,13 @@ import com.elvishew.xlog.printer.AndroidPrinter
import com.elvishew.xlog.printer.Printer import com.elvishew.xlog.printer.Printer
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.domain.DomainModule import eu.kanade.domain.DomainModule
import eu.kanade.domain.SYDomainModule import eu.kanade.domain.SYDomainModule
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
@@ -48,8 +47,11 @@ import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.di.AppModule import eu.kanade.tachiyomi.di.AppModule
import eu.kanade.tachiyomi.di.InjektKoinBridge
import eu.kanade.tachiyomi.di.PreferenceModule import eu.kanade.tachiyomi.di.PreferenceModule
import eu.kanade.tachiyomi.di.SYPreferenceModule import eu.kanade.tachiyomi.di.SYPreferenceModule
import eu.kanade.tachiyomi.di.importModule
import eu.kanade.tachiyomi.di.initExpensiveComponents
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
@@ -57,8 +59,6 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
import exh.log.CrashlyticsPrinter import exh.log.CrashlyticsPrinter
import exh.log.EHLogLevel import exh.log.EHLogLevel
@@ -71,6 +71,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import mihon.core.firebase.Firebase
import mihon.core.migration.Migrator import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
@@ -91,6 +92,7 @@ import java.util.Locale
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy() private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -99,11 +101,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
// SY -->
if (!isDevFlavor) {
Firebase.crashlytics.setCrashlyticsCollectionEnabled(isReleaseBuildType)
}
// SY <--
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
// TLS 1.3 support for Android < 10 // TLS 1.3 support for Android < 10
@@ -123,11 +120,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
// SY --> // SY -->
Injekt.importModule(SYPreferenceModule(this)) Injekt.importModule(SYPreferenceModule(this))
Injekt.importModule(SYDomainModule()) Injekt.importModule(SYDomainModule())
InjektKoinBridge.startKoin(this)
initExpensiveComponents(this)
// SY <-- // SY <--
setupExhLogging() // EXH logging setupExhLogging() // EXH logging
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope)
setupNotificationChannels() setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@@ -27,11 +27,11 @@ import okio.sink
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.data.manga.MangaMapper
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.GetMergedManga import tachiyomi.domain.manga.interactor.GetMergedManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -48,6 +48,7 @@ class BackupCreator(
private val parser: ProtoBuf = Injekt.get(), private val parser: ProtoBuf = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
private val backupPreferences: BackupPreferences = Injekt.get(), private val backupPreferences: BackupPreferences = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
@@ -85,15 +86,13 @@ class BackupCreator(
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error)) throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
} }
val backupManga = backupMangas( val nonFavoriteManga = if (options.readEntries) mangaRepository.getReadMangaNotInLibrary() else emptyList()
getFavorites.await() /* SY --> */ + // SY -->
if (options.readEntries) { val mergedManga = getMergedManga.await()
handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) } // SY <--
} else { val backupManga =
emptyList() backupMangas(getFavorites.await() + nonFavoriteManga /* SY --> */ + mergedManga /* SY <-- */, options)
} + getMergedManga.await(), // SY <--
options,
)
val backup = Backup( val backup = Backup(
backupManga = backupManga, backupManga = backupManga,
backupCategories = backupCategories(options), backupCategories = backupCategories(options),
@@ -11,13 +11,13 @@ data class BackupOptions(
val chapters: Boolean = true, val chapters: Boolean = true,
val tracking: Boolean = true, val tracking: Boolean = true,
val history: Boolean = true, val history: Boolean = true,
val readEntries: Boolean = true,
val appSettings: Boolean = true, val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true, val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true, val sourceSettings: Boolean = true,
val privateSettings: Boolean = false, val privateSettings: Boolean = false,
// SY --> // SY -->
val customInfo: Boolean = true, val customInfo: Boolean = true,
val readEntries: Boolean = true,
val savedSearches: Boolean = true, val savedSearches: Boolean = true,
// SY <-- // SY <--
) { ) {
@@ -28,13 +28,13 @@ data class BackupOptions(
chapters, chapters,
tracking, tracking,
history, history,
readEntries,
appSettings, appSettings,
extensionRepoSettings, extensionRepoSettings,
sourceSettings, sourceSettings,
privateSettings, privateSettings,
// SY --> // SY -->
customInfo, customInfo,
readEntries,
savedSearches, savedSearches,
// SY <-- // SY <--
) )
@@ -72,6 +72,12 @@ data class BackupOptions(
getter = BackupOptions::categories, getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) }, setter = { options, enabled -> options.copy(categories = enabled) },
), ),
Entry(
label = MR.strings.non_library_settings,
getter = BackupOptions::readEntries,
setter = { options, enabled -> options.copy(readEntries = enabled) },
enabled = { it.libraryEntries },
),
// SY --> // SY -->
Entry( Entry(
label = SYMR.strings.custom_entry_info, label = SYMR.strings.custom_entry_info,
@@ -79,12 +85,6 @@ data class BackupOptions(
setter = { options, enabled -> options.copy(customInfo = enabled) }, setter = { options, enabled -> options.copy(customInfo = enabled) },
enabled = { it.libraryEntries }, enabled = { it.libraryEntries },
), ),
Entry(
label = SYMR.strings.all_read_entries,
getter = BackupOptions::readEntries,
setter = { options, enabled -> options.copy(readEntries = enabled) },
enabled = { it.libraryEntries },
),
Entry( Entry(
label = SYMR.strings.saved_searches, label = SYMR.strings.saved_searches,
getter = BackupOptions::savedSearches, getter = BackupOptions::savedSearches,
@@ -123,13 +123,13 @@ data class BackupOptions(
chapters = array[2], chapters = array[2],
tracking = array[3], tracking = array[3],
history = array[4], history = array[4],
appSettings = array[5], readEntries = array[5],
extensionRepoSettings = array[6], appSettings = array[6],
sourceSettings = array[7], extensionRepoSettings = array[7],
privateSettings = array[8], sourceSettings = array[8],
privateSettings = array[9],
// SY --> // SY -->
customInfo = array[9], customInfo = array[10],
readEntries = array[10],
savedSearches = array[11], savedSearches = array[11],
// SY <-- // SY <--
) )
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -129,13 +130,15 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
0.0 -> "0 ★" 0.0 -> "0 ★"
else -> "${((score + 10) / 20).toInt()}" else -> "${((score + 10) / 20).toInt()}"
} }
POINT_3 -> when { POINT_3 -> when {
score == 0.0 -> "0" score == 0.0 -> "0"
score <= 35 -> "😦" score <= 35 -> "😦"
score <= 60 -> "😐" score <= 60 -> "😐"
else -> "😊" else -> "😊"
} }
else -> track.toAnilistScore()
else -> track.toApiScore()
} }
} }
@@ -217,7 +220,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
interceptor.setAuth(oauth) interceptor.setAuth(oauth)
val (username, scoreType) = api.getCurrentUser() val (username, scoreType) = api.getCurrentUser()
scorePreference.set(scoreType) scorePreference.set(scoreType)
saveCredentials(username.toString(), oauth.access_token) saveCredentials(username.toString(), oauth.accessToken)
} catch (e: Throwable) { } catch (e: Throwable) {
logout() logout()
} }
@@ -229,13 +232,13 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
interceptor.setAuth(null) interceptor.setAuth(null)
} }
fun saveOAuth(oAuth: OAuth?) { fun saveOAuth(alOAuth: ALOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) trackPreferences.trackToken(this).set(json.encodeToString(alOAuth))
} }
fun loadOAuth(): OAuth? { fun loadOAuth(): ALOAuth? {
return try { return try {
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<ALOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
@@ -13,14 +18,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -28,7 +25,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@@ -59,7 +55,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
putJsonObject("variables") { putJsonObject("variables") {
put("mangaId", track.remote_id) put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("status", track.toAnilistStatus()) put("status", track.toApiStatus())
} }
} }
with(json) { with(json) {
@@ -70,10 +66,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<ALAddMangaResult>()
.let { .let {
track.library_id = track.library_id = it.data.entry.id
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track track
} }
} }
@@ -103,7 +98,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
putJsonObject("variables") { putJsonObject("variables") {
put("listId", track.library_id) put("listId", track.library_id)
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("status", track.toAnilistStatus()) put("status", track.toApiStatus())
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date)) put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
@@ -135,6 +130,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
.awaitSuccess() .awaitSuccess()
} }
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val query = """ val query = """
@@ -177,14 +173,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<ALSearchResult>()
.let { response -> .data.page.media
val data = response["data"]!!.jsonObject .map { it.toALManga().toTrack() }
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
}
} }
} }
} }
@@ -247,14 +238,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<ALUserListMangaQueryResult>()
.let { response -> .data.page.mediaList
val data = response["data"]!!.jsonObject .map { it.toALUserManga() }
val page = data["Page"]!!.jsonObject .firstOrNull()
val media = page["mediaList"]!!.jsonArray ?.toTrack()
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
}
} }
} }
} }
@@ -263,8 +251,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return findLibManga(track, userId) ?: throw Exception("Could not find manga") return findLibManga(track, userId) ?: throw Exception("Could not find manga")
} }
fun createOAuth(token: String): OAuth { fun createOAuth(token: String): ALOAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
} }
suspend fun getCurrentUser(): Pair<Int, String> { suspend fun getCurrentUser(): Pair<Int, String> {
@@ -291,61 +279,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<ALCurrentUserResult>()
.let { .let {
val data = it["data"]!!.jsonObject val viewer = it.data.viewer
val viewer = data["Viewer"]!!.jsonObject Pair(viewer.id, viewer.mediaListOptions.scoreFormat)
Pair(
viewer["id"]!!.jsonPrimitive.int,
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
)
} }
} }
} }
} }
private fun jsonToALManga(struct: JsonObject): ALManga {
return ALManga(
struct["id"]!!.jsonPrimitive.long,
struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
struct["description"]!!.jsonPrimitive.contentOrNull,
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.longOrNull ?: 0,
struct["averageScore"]?.jsonPrimitive?.intOrNull ?: -1,
)
}
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(
struct["id"]!!.jsonPrimitive.long,
struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject),
)
}
private fun parseDate(struct: JsonObject, dateKey: String): Long {
return try {
return LocalDate
.of(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
)
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject { private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) { if (dateValue == 0L) {
return buildJsonObject { return buildJsonObject {
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.anilist.dto.isExpired
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
@@ -13,7 +15,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute * Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
* before its original expiration date. * before its original expiration date.
*/ */
private var oauth: OAuth? = null private var oauth: ALOAuth? = null
set(value) { set(value) {
field = value?.copy(expires = value.expires * 1000 - 60 * 1000) field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
} }
@@ -40,7 +42,7 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
.header("User-Agent", "TachiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .header("User-Agent", "TachiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build() .build()
@@ -51,8 +53,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
* Called when the user authenticates with Anilist for the first time. Sets the refresh token * Called when the user authenticates with Anilist for the first time. Sets the refresh token
* and the oauth object. * and the oauth object.
*/ */
fun setAuth(oauth: OAuth?) { fun setAuth(oauth: ALOAuth?) {
token = oauth?.access_token token = oauth?.accessToken
this.oauth = oauth this.oauth = oauth
anilist.saveOAuth(oauth) anilist.saveOAuth(oauth)
} }
@@ -1,126 +0,0 @@
package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.Serializable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import tachiyomi.domain.track.model.Track as DomainTrack
data class ALManga(
val remote_id: Long,
val title_user_pref: String,
val image_url_lge: String,
val description: String?,
val format: String,
val publishing_status: String,
val start_date_fuzzy: Long,
val total_chapters: Long,
val average_score: Int,
) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = this@ALManga.remote_id
title = title_user_pref
total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge
summary = description?.htmlDecode() ?: ""
score = average_score.toDouble()
tracking_url = AnilistApi.mangaUrl(remote_id)
publishing_status = this@ALManga.publishing_status
publishing_type = format
if (start_date_fuzzy != 0L) {
start_date = try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(start_date_fuzzy)
} catch (e: Exception) {
""
}
}
}
}
data class ALUserManga(
val library_id: Long,
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga,
) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remote_id
title = manga.title_user_pref
status = toTrackStatus()
score = score_raw.toDouble()
started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read.toDouble()
library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters
}
private fun toTrackStatus() = when (list_status) {
"CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.ON_HOLD
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLAN_TO_READ
"REPEATING" -> Anilist.REREADING
else -> throw NotImplementedError("Unknown status: $list_status")
}
}
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long,
)
fun OAuth.isExpired() = System.currentTimeMillis() > expires
fun Track.toAnilistStatus() = when (status) {
Anilist.READING -> "CURRENT"
Anilist.COMPLETED -> "COMPLETED"
Anilist.ON_HOLD -> "PAUSED"
Anilist.DROPPED -> "DROPPED"
Anilist.PLAN_TO_READ -> "PLANNING"
Anilist.REREADING -> "REPEATING"
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: TrackPreferences by injectLazy()
fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
// 10 point
"POINT_10" -> (score.toInt() / 10).toString()
// 100 point
"POINT_100" -> score.toInt().toString()
// 5 stars
"POINT_5" -> when {
score == 0.0 -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley
"POINT_3" -> when {
score == 0.0 -> "0"
score <= 35 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw NotImplementedError("Unknown score type")
}
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.track.anilist
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack
fun Track.toApiStatus() = when (status) {
Anilist.READING -> "CURRENT"
Anilist.COMPLETED -> "COMPLETED"
Anilist.ON_HOLD -> "PAUSED"
Anilist.DROPPED -> "DROPPED"
Anilist.PLAN_TO_READ -> "PLANNING"
Anilist.REREADING -> "REPEATING"
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: TrackPreferences by injectLazy()
fun DomainTrack.toApiScore(): String = when (preferences.anilistScoreType().get()) {
// 10 point
"POINT_10" -> (score.toInt() / 10).toString()
// 100 point
"POINT_100" -> score.toInt().toString()
// 5 stars
"POINT_5" -> when {
score == 0.0 -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
// Smiley
"POINT_3" -> when {
score == 0.0 -> "0"
score <= 35 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw NotImplementedError("Unknown score type")
}
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ALAddMangaResult(
val data: ALAddMangaData,
)
@Serializable
data class ALAddMangaData(
@SerialName("SaveMediaListEntry")
val entry: ALAddMangaEntry,
)
@Serializable
data class ALAddMangaEntry(
val id: Long,
)
@@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.Serializable
import java.time.LocalDate
import java.time.ZoneId
@Serializable
data class ALFuzzyDate(
val year: Int?,
val month: Int?,
val day: Int?,
) {
fun toEpochMilli(): Long = try {
LocalDate.of(year!!, month!!, day!!)
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
} catch (_: Exception) {
0L
}
}
@@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.lang.htmlDecode
import java.text.SimpleDateFormat
import java.util.Locale
data class ALManga(
val remoteId: Long,
val title: String,
val imageUrl: String,
val description: String?,
val format: String,
val publishingStatus: String,
val startDateFuzzy: Long,
val totalChapters: Long,
val averageScore: Int,
) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = remoteId
title = this@ALManga.title
total_chapters = totalChapters
cover_url = imageUrl
summary = description?.htmlDecode() ?: ""
score = averageScore.toDouble()
tracking_url = AnilistApi.mangaUrl(remote_id)
publishing_status = publishingStatus
publishing_type = format
if (startDateFuzzy != 0L) {
start_date = try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(startDateFuzzy)
} catch (e: IllegalArgumentException) {
""
}
}
}
}
data class ALUserManga(
val libraryId: Long,
val listStatus: String,
val scoreRaw: Int,
val chaptersRead: Int,
val startDateFuzzy: Long,
val completedDateFuzzy: Long,
val manga: ALManga,
) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remoteId
title = manga.title
status = toTrackStatus()
score = scoreRaw.toDouble()
started_reading_date = startDateFuzzy
finished_reading_date = completedDateFuzzy
last_chapter_read = chaptersRead.toDouble()
library_id = libraryId
total_chapters = manga.totalChapters
}
private fun toTrackStatus() = when (listStatus) {
"CURRENT" -> Anilist.READING
"COMPLETED" -> Anilist.COMPLETED
"PAUSED" -> Anilist.ON_HOLD
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLAN_TO_READ
"REPEATING" -> Anilist.REREADING
else -> throw NotImplementedError("Unknown status: $listStatus")
}
}
@@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ALOAuth(
@SerialName("access_token")
val accessToken: String,
@SerialName("token_type")
val tokenType: String,
val expires: Long,
@SerialName("expires_in")
val expiresIn: Long,
)
fun ALOAuth.isExpired() = System.currentTimeMillis() > expires
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ALSearchResult(
val data: ALSearchPage,
)
@Serializable
data class ALSearchPage(
@SerialName("Page")
val page: ALSearchMedia,
)
@Serializable
data class ALSearchMedia(
val media: List<ALSearchItem>,
)
@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.Serializable
@Serializable
data class ALSearchItem(
val id: Long,
val title: ALItemTitle,
val coverImage: ItemCover,
val description: String?,
val format: String,
val status: String?,
val startDate: ALFuzzyDate,
val chapters: Long?,
val averageScore: Int?,
) {
fun toALManga(): ALManga = ALManga(
remoteId = id,
title = title.userPreferred,
imageUrl = coverImage.large,
description = description,
format = format.replace("_", "-"),
publishingStatus = status ?: "",
startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1,
)
}
@Serializable
data class ALItemTitle(
val userPreferred: String,
)
@Serializable
data class ItemCover(
val large: String,
)
@@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ALCurrentUserResult(
val data: ALUserViewer,
)
@Serializable
data class ALUserViewer(
@SerialName("Viewer")
val viewer: ALUserViewerData,
)
@Serializable
data class ALUserViewerData(
val id: Int,
val mediaListOptions: ALUserListOptions,
)
@Serializable
data class ALUserListOptions(
val scoreFormat: String,
)
@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.track.anilist.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ALUserListMangaQueryResult(
val data: ALUserListMangaPage,
)
@Serializable
data class ALUserListMangaPage(
@SerialName("Page")
val page: ALUserListMediaList,
)
@Serializable
data class ALUserListMediaList(
val mediaList: List<ALUserListItem>,
)
@Serializable
data class ALUserListItem(
val id: Long,
val status: String,
val scoreRaw: Int,
val progress: Int,
val startedAt: ALFuzzyDate,
val completedAt: ALFuzzyDate,
val media: ALSearchItem,
) {
fun toALUserManga(): ALUserManga {
return ALUserManga(
libraryId = this@ALUserListItem.id,
listStatus = status,
scoreRaw = scoreRaw,
chaptersRead = progress,
startDateFuzzy = startedAt.toEpochMilli(),
completedDateFuzzy = completedAt.toEpochMilli(),
manga = media.toALManga(),
)
}
}
@@ -5,6 +5,7 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -75,8 +76,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
} }
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track) val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack!!) track.copyPersonalFrom(remoteStatusTrack)
api.findLibManga(track)?.let { remoteTrack -> api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
} }
@@ -112,19 +113,19 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
try { try {
val oauth = api.accessToken(code) val oauth = api.accessToken(code)
interceptor.newAuth(oauth) interceptor.newAuth(oauth)
saveCredentials(oauth.user_id.toString(), oauth.access_token) saveCredentials(oauth.userId.toString(), oauth.accessToken)
} catch (e: Throwable) { } catch (e: Throwable) {
logout() logout()
} }
} }
fun saveToken(oauth: OAuth?) { fun saveToken(oauth: BGMOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oauth)) trackPreferences.trackToken(this).set(json.encodeToString(oauth))
} }
fun restoreToken(): OAuth? { fun restoreToken(): BGMOAuth? {
return try { return try {
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -3,20 +3,16 @@ package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -40,7 +36,7 @@ class BangumiApi(
return withIOContext { return withIOContext {
val body = FormBody.Builder() val body = FormBody.Builder()
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toApiStatus())
.build() .build()
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body)) authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
.awaitSuccess() .awaitSuccess()
@@ -53,7 +49,7 @@ class BangumiApi(
// read status update // read status update
val sbody = FormBody.Builder() val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toApiStatus())
.build() .build()
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody)) authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
.awaitSuccess() .awaitSuccess()
@@ -63,10 +59,7 @@ class BangumiApi(
.add("watched_eps", track.last_chapter_read.toInt().toString()) .add("watched_eps", track.last_chapter_read.toInt().toString())
.build() .build()
authClient.newCall( authClient.newCall(
POST( POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body),
"$API_URL/subject/${track.remote_id}/update/watched_eps",
body = body,
),
).awaitSuccess() ).awaitSuccess()
track track
@@ -80,44 +73,19 @@ class BangumiApi(
.buildUpon() .buildUpon()
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
authClient.newCall(GET(url.toString())) with(json) {
.awaitSuccess() authClient.newCall(GET(url.toString()))
.use { .awaitSuccess()
var responseBody = it.body.string() .parseAs<BGMSearchResult>()
if (responseBody.isEmpty()) { .let { result ->
throw Exception("Null Response") if (result.code == 404) emptyList<TrackSearch>()
}
if (responseBody.contains("\"code\":404")) {
responseBody = "{\"results\":0,\"list\":[]}"
}
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }
?.map { jsonToSearch(it.jsonObject) }.orEmpty()
}
}
}
private fun jsonToSearch(obj: JsonObject): TrackSearch { result.list
val coverUrl = if (obj["images"] is JsonObject) { ?.filter { it.type == 1 }
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: "" ?.map { it.toTrackSearch(trackId) }
} else { .orEmpty()
// Sometimes JsonNull }
"" }
}
val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.long
} else {
0
}
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.doubleOrNull ?: -1.0
return TrackSearch.create(trackId).apply {
remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = coverUrl
summary = obj["name"]!!.jsonPrimitive.content
score = rating
tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
} }
} }
@@ -126,8 +94,8 @@ class BangumiApi(
with(json) { with(json) {
authClient.newCall(GET("$API_URL/subject/${track.remote_id}")) authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<BGMSearchItem>()
.let { jsonToSearch(it) } .toTrackSearch(trackId)
} }
} }
} }
@@ -142,25 +110,23 @@ class BangumiApi(
.build() .build()
// TODO: get user readed chapter here // TODO: get user readed chapter here
val response = authClient.newCall(requestUserRead).awaitSuccess() with(json) {
val responseBody = response.body.string() authClient.newCall(requestUserRead)
if (responseBody.isEmpty()) { .awaitSuccess()
throw Exception("Null Response") .parseAs<BGMCollectionResponse>()
} .let {
if (responseBody.contains("\"code\":400")) { if (it.code == 400) return@let null
null
} else { track.status = it.status?.id!!
json.decodeFromString<Collection>(responseBody).let { track.last_chapter_read = it.epStatus!!.toDouble()
track.status = it.status?.id!! track.score = it.rating!!
track.last_chapter_read = it.ep_status!!.toDouble() track
track.score = it.rating!! }
track
}
} }
} }
} }
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): BGMOAuth {
return withIOContext { return withIOContext {
with(json) { with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(accessTokenRequest(code))
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -14,7 +16,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.
*/ */
private var oauth: OAuth? = bangumi.restoreToken() private var oauth: BGMOAuth? = bangumi.restoreToken()
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -22,9 +24,9 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi") val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!)) val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
if (response.isSuccessful) { if (response.isSuccessful) {
newAuth(json.decodeFromString<OAuth>(response.body.string())) newAuth(json.decodeFromString<BGMOAuth>(response.body.string()))
} else { } else {
response.close() response.close()
} }
@@ -38,28 +40,28 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
.apply { .apply {
if (originalRequest.method == "GET") { if (originalRequest.method == "GET") {
val newUrl = originalRequest.url.newBuilder() val newUrl = originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.access_token) .addQueryParameter("access_token", currAuth.accessToken)
.build() .build()
url(newUrl) url(newUrl)
} else { } else {
post(addToken(currAuth.access_token, originalRequest.body as FormBody)) post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
} }
} }
.build() .build()
.let(chain::proceed) .let(chain::proceed)
} }
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: BGMOAuth?) {
this.oauth = if (oauth == null) { this.oauth = if (oauth == null) {
null null
} else { } else {
OAuth( BGMOAuth(
oauth.access_token, oauth.accessToken,
oauth.token_type, oauth.tokenType,
System.currentTimeMillis() / 1000, System.currentTimeMillis() / 1000,
oauth.expires_in, oauth.expiresIn,
oauth.refresh_token, oauth.refreshToken,
this.oauth?.user_id, this.oauth?.userId,
) )
} }
@@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track
import kotlinx.serialization.Serializable
@Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = "",
)
@Serializable
data class Collection(
val `private`: Int? = 0,
val comment: String? = "",
val ep_status: Int? = 0,
val lasttouch: Int? = 0,
val rating: Double? = 0.0,
val status: Status? = Status(),
val tag: List<String?>? = emptyList(),
val user: User? = User(),
val vol_status: Int? = 0,
)
@Serializable
data class Status(
val id: Long? = 0,
val name: String? = "",
val type: String? = "",
)
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
val usergroup: Int? = 0,
val username: String? = "",
)
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long = System.currentTimeMillis() / 1000,
val expires_in: Long,
val refresh_token: String?,
val user_id: Long?,
)
// Access token refresh before expired
fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
fun Track.toBangumiStatus() = when (status) {
Bangumi.READING -> "do"
Bangumi.COMPLETED -> "collect"
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLAN_TO_READ -> "wish"
else -> throw NotImplementedError("Unknown status: $status")
}
@@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toApiStatus() = when (status) {
Bangumi.READING -> "do"
Bangumi.COMPLETED -> "collect"
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLAN_TO_READ -> "wish"
else -> throw NotImplementedError("Unknown status: $status")
}
@@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BGMCollectionResponse(
val code: Int?,
val `private`: Int? = 0,
val comment: String? = "",
@SerialName("ep_status")
val epStatus: Int? = 0,
@SerialName("lasttouch")
val lastTouch: Int? = 0,
val rating: Double? = 0.0,
val status: Status? = Status(),
val tag: List<String?>? = emptyList(),
val user: User? = User(),
@SerialName("vol_status")
val volStatus: Int? = 0,
)
@Serializable
data class Status(
val id: Long? = 0,
val name: String? = "",
val type: String? = "",
)
@@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BGMOAuth(
@SerialName("access_token")
val accessToken: String,
@SerialName("token_type")
val tokenType: String,
@SerialName("created_at")
val createdAt: Long = System.currentTimeMillis() / 1000,
@SerialName("expires_in")
val expiresIn: Long,
@SerialName("refresh_token")
val refreshToken: String?,
@SerialName("user_id")
val userId: Long?,
)
// Access token refresh before expired
fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
@@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BGMSearchResult(
val list: List<BGMSearchItem>?,
val code: Int?,
)
@Serializable
data class BGMSearchItem(
val id: Long,
@SerialName("name_cn")
val nameCn: String,
val name: String,
val type: Int,
val images: BGMSearchItemCovers?,
@SerialName("eps_count")
val epsCount: Long?,
val rating: BGMSearchItemRating?,
val url: String,
) {
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
remote_id = this@BGMSearchItem.id
title = nameCn
cover_url = images?.common ?: ""
summary = this@BGMSearchItem.name
score = rating?.score ?: -1.0
tracking_url = url
total_chapters = epsCount ?: 0
}
}
@Serializable
data class BGMSearchItemCovers(
val common: String?,
)
@Serializable
data class BGMSearchItemRating(
val score: Double?,
)
@@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = "",
)
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
@SerialName("usergroup")
val userGroup: Int? = 0,
val username: String? = "",
)
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -142,13 +143,13 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
return getPassword() return getPassword()
} }
fun saveToken(oauth: OAuth?) { fun saveToken(oauth: KitsuOAuth?) {
trackPreferences.trackToken(this).set(json.encodeToString(oauth)) trackPreferences.trackToken(this).set(json.encodeToString(oauth))
} }
fun restoreToken(): OAuth? { fun restoreToken(): KitsuOAuth? {
return try { return try {
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get()) json.decodeFromString<KitsuOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.data.track.kitsu
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddMangaResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAlgoliaSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@@ -10,12 +16,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.FormBody import okhttp3.FormBody
@@ -43,7 +44,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
putJsonObject("data") { putJsonObject("data") {
put("type", "libraryEntries") put("type", "libraryEntries")
putJsonObject("attributes") { putJsonObject("attributes") {
put("status", track.toKitsuStatus()) put("status", track.toApiStatus())
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
} }
putJsonObject("relationships") { putJsonObject("relationships") {
@@ -67,18 +68,14 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
authClient.newCall( authClient.newCall(
POST( POST(
"${BASE_URL}library-entries", "${BASE_URL}library-entries",
headers = headersOf( headers = headersOf("Content-Type", VND_API_JSON),
"Content-Type", body = data.toString().toRequestBody(VND_JSON_MEDIA_TYPE),
"application/vnd.api+json",
),
body = data.toString()
.toRequestBody("application/vnd.api+json".toMediaType()),
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuAddMangaResult>()
.let { .let {
track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long track.remote_id = it.data.id
track track
} }
} }
@@ -92,63 +89,50 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
put("type", "libraryEntries") put("type", "libraryEntries")
put("id", track.remote_id) put("id", track.remote_id)
putJsonObject("attributes") { putJsonObject("attributes") {
put("status", track.toKitsuStatus()) put("status", track.toApiStatus())
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("ratingTwenty", track.toKitsuScore()) put("ratingTwenty", track.toApiScore())
put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
} }
} }
} }
with(json) { authClient.newCall(
authClient.newCall( Request.Builder()
Request.Builder() .url("${BASE_URL}library-entries/${track.remote_id}")
.url("${BASE_URL}library-entries/${track.remote_id}") .headers(
.headers( headersOf("Content-Type", VND_API_JSON),
headersOf( )
"Content-Type", .patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE))
"application/vnd.api+json", .build(),
), )
) .awaitSuccess()
.patch(
data.toString().toRequestBody("application/vnd.api+json".toMediaType()), track
)
.build(),
)
.awaitSuccess()
.parseAs<JsonObject>()
.let {
track
}
}
} }
} }
suspend fun removeLibManga(track: DomainTrack) { suspend fun removeLibManga(track: DomainTrack) {
withIOContext { withIOContext {
authClient authClient.newCall(
.newCall( DELETE(
DELETE( "${BASE_URL}library-entries/${track.remoteId}",
"${BASE_URL}library-entries/${track.remoteId}", headers = headersOf("Content-Type", VND_API_JSON),
headers = headersOf( ),
"Content-Type", )
"application/vnd.api+json",
),
),
)
.awaitSuccess() .awaitSuccess()
} }
} }
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
with(json) { with(json) {
authClient.newCall(GET(ALGOLIA_KEY_URL)) authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuSearchResult>()
.let { .let {
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content algoliaSearch(it.media.key, query)
algoliaSearch(key, query)
} }
} }
} }
@@ -174,13 +158,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuAlgoliaSearchResult>()
.let { .hits
it["hits"]!!.jsonArray .filter { it.subtype != "novel" }
.map { KitsuSearchManga(it.jsonObject) } .map { it.toTrack() }
.filter { it.subType != "novel" }
.map { it.toTrack() }
}
} }
} }
} }
@@ -194,12 +175,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) { with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuListSearchResult>()
.let { .let {
val data = it["data"]!!.jsonArray if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
if (data.size > 0) { it.firstToTrack()
val manga = it["included"]!!.jsonArray[0].jsonObject
KitsuLibManga(data[0].jsonObject, manga).toTrack()
} else { } else {
null null
} }
@@ -217,12 +196,10 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) { with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuListSearchResult>()
.let { .let {
val data = it["data"]!!.jsonArray if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
if (data.size > 0) { it.firstToTrack()
val manga = it["included"]!!.jsonArray[0].jsonObject
KitsuLibManga(data[0].jsonObject, manga).toTrack()
} else { } else {
throw Exception("Could not find manga") throw Exception("Could not find manga")
} }
@@ -231,7 +208,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
suspend fun login(username: String, password: String): OAuth { suspend fun login(username: String, password: String): KitsuOAuth {
return withIOContext { return withIOContext {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("username", username) .add("username", username)
@@ -256,10 +233,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) { with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<KitsuCurrentUserResult>()
.let { .data[0]
it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content .id
}
} }
} }
} }
@@ -279,6 +255,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" + "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
private const val VND_API_JSON = "application/vnd.api+json"
private val VND_JSON_MEDIA_TYPE = VND_API_JSON.toMediaType()
fun mangaUrl(remoteId: Long): String { fun mangaUrl(remoteId: Long): String {
return BASE_MANGA_URL + remoteId return BASE_MANGA_URL + remoteId
} }
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.track.kitsu package eu.kanade.tachiyomi.data.track.kitsu
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.kitsu.dto.isExpired
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
@@ -13,14 +15,14 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
/** /**
* OAuth object used for authenticated requests. * OAuth object used for authenticated requests.
*/ */
private var oauth: OAuth? = kitsu.restoreToken() private var oauth: KitsuOAuth? = kitsu.restoreToken()
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu") val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
val refreshToken = currAuth.refresh_token!! val refreshToken = currAuth.refreshToken!!
// Refresh access token if expired. // Refresh access token if expired.
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
@@ -34,7 +36,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
// Add the authorization header to the original request. // Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
.header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
@@ -43,7 +45,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
return chain.proceed(authRequest) return chain.proceed(authRequest)
} }
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: KitsuOAuth?) {
this.oauth = oauth this.oauth = oauth
kitsu.saveToken(oauth) kitsu.saveToken(oauth)
} }
@@ -1,121 +0,0 @@
package eu.kanade.tachiyomi.data.track.kitsu
import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class KitsuSearchManga(obj: JsonObject) {
val id = obj["id"]!!.jsonPrimitive.long
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.longOrNull
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
val original = try {
obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
} catch (e: IllegalArgumentException) {
// posterImage is sometimes a jsonNull object instead
null
}
private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull
private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull()
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(Date(it.toLong() * 1000))
}
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
@CallSuper
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
remote_id = this@KitsuSearchManga.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = original ?: ""
summary = synopsis ?: ""
tracking_url = KitsuApi.mangaUrl(remote_id)
score = rating ?: -1.0
publishing_status = if (endDate == null) {
"Publishing"
} else {
"Finished"
}
publishing_type = subType ?: ""
start_date = startDate ?: ""
}
}
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
val id = manga["id"]!!.jsonPrimitive.int
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.longOrNull
val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
private val libraryId = obj["id"]!!.jsonPrimitive.long
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
remote_id = libraryId
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = original
summary = synopsis
tracking_url = KitsuApi.mangaUrl(remote_id)
publishing_status = this@KitsuLibManga.status
publishing_type = type
start_date = startDate
started_reading_date = KitsuDateHelper.parse(startedAt)
finished_reading_date = KitsuDateHelper.parse(finishedAt)
status = toTrackStatus()
score = ratingTwenty?.let { it.toInt() / 2.0 } ?: 0.0
last_chapter_read = progress.toDouble()
}
private fun toTrackStatus() = when (status) {
"current" -> Kitsu.READING
"completed" -> Kitsu.COMPLETED
"on_hold" -> Kitsu.ON_HOLD
"dropped" -> Kitsu.DROPPED
"planned" -> Kitsu.PLAN_TO_READ
else -> throw Exception("Unknown status")
}
}
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val expires_in: Long,
val refresh_token: String?,
)
fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
fun Track.toKitsuStatus() = when (status) {
Kitsu.READING -> "current"
Kitsu.COMPLETED -> "completed"
Kitsu.ON_HOLD -> "on_hold"
Kitsu.DROPPED -> "dropped"
Kitsu.PLAN_TO_READ -> "planned"
else -> throw Exception("Unknown status")
}
fun Track.toKitsuScore(): String? {
return if (score > 0) (score * 2).toInt().toString() else null
}
@@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.data.track.kitsu
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toApiStatus() = when (status) {
Kitsu.READING -> "current"
Kitsu.COMPLETED -> "completed"
Kitsu.ON_HOLD -> "on_hold"
Kitsu.DROPPED -> "dropped"
Kitsu.PLAN_TO_READ -> "planned"
else -> throw Exception("Unknown status")
}
fun Track.toApiScore(): String? {
return if (score > 0) (score * 2).toInt().toString() else null
}
@@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import kotlinx.serialization.Serializable
@Serializable
data class KitsuAddMangaResult(
val data: KitsuAddMangaItem,
)
@Serializable
data class KitsuAddMangaItem(
val id: Long,
)
@@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi
import eu.kanade.tachiyomi.data.track.kitsu.KitsuDateHelper
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.Serializable
@Serializable
data class KitsuListSearchResult(
val data: List<KitsuListSearchItemData>,
val included: List<KitsuListSearchItemIncluded> = emptyList(),
) {
fun firstToTrack(): TrackSearch {
require(data.isNotEmpty()) { "Missing User data from Kitsu" }
require(included.isNotEmpty()) { "Missing Manga data from Kitsu" }
val userData = data[0]
val userDataAttrs = userData.attributes
val manga = included[0].attributes
return TrackSearch.create(TrackerManager.KITSU).apply {
remote_id = userData.id
title = manga.canonicalTitle
total_chapters = manga.chapterCount ?: 0
cover_url = manga.posterImage?.original ?: ""
summary = manga.synopsis ?: ""
tracking_url = KitsuApi.mangaUrl(remote_id)
publishing_status = manga.status
publishing_type = manga.mangaType ?: ""
start_date = userDataAttrs.startedAt ?: ""
started_reading_date = KitsuDateHelper.parse(userDataAttrs.startedAt)
finished_reading_date = KitsuDateHelper.parse(userDataAttrs.finishedAt)
status = when (userDataAttrs.status) {
"current" -> Kitsu.READING
"completed" -> Kitsu.COMPLETED
"on_hold" -> Kitsu.ON_HOLD
"dropped" -> Kitsu.DROPPED
"planned" -> Kitsu.PLAN_TO_READ
else -> throw Exception("Unknown status")
}
score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0
last_chapter_read = userDataAttrs.progress.toDouble()
}
}
}
@Serializable
data class KitsuListSearchItemData(
val id: Long,
val attributes: KitsuListSearchItemDataAttributes,
)
@Serializable
data class KitsuListSearchItemDataAttributes(
val status: String,
val startedAt: String?,
val finishedAt: String?,
val ratingTwenty: Int?,
val progress: Int,
)
@Serializable
data class KitsuListSearchItemIncluded(
val id: Long,
val attributes: KitsuListSearchItemIncludedAttributes,
)
@Serializable
data class KitsuListSearchItemIncludedAttributes(
val canonicalTitle: String,
val chapterCount: Long?,
val mangaType: String?,
val posterImage: KitsuSearchItemCover?,
val synopsis: String?,
val startDate: String?,
val status: String,
)
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class KitsuOAuth(
@SerialName("access_token")
val accessToken: String,
@SerialName("token_type")
val tokenType: String,
@SerialName("created_at")
val createdAt: Long,
@SerialName("expires_in")
val expiresIn: Long,
@SerialName("refresh_token")
val refreshToken: String?,
)
fun KitsuOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
@@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.kitsu.KitsuApi
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Serializable
data class KitsuSearchResult(
val media: KitsuSearchResultData,
)
@Serializable
data class KitsuSearchResultData(
val key: String,
)
@Serializable
data class KitsuAlgoliaSearchResult(
val hits: List<KitsuAlgoliaSearchItem>,
)
@Serializable
data class KitsuAlgoliaSearchItem(
val id: Long,
val canonicalTitle: String,
val chapterCount: Long?,
val subtype: String?,
val posterImage: KitsuSearchItemCover?,
val synopsis: String?,
val averageRating: Double?,
val startDate: Long?,
val endDate: Long?,
) {
fun toTrack(): TrackSearch {
return TrackSearch.create(TrackerManager.KITSU).apply {
remote_id = this@KitsuAlgoliaSearchItem.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = posterImage?.original ?: ""
summary = synopsis ?: ""
tracking_url = KitsuApi.mangaUrl(remote_id)
score = averageRating ?: -1.0
publishing_status = if (endDate == null) "Publishing" else "Finished"
publishing_type = subtype ?: ""
start_date = startDate?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(Date(it * 1000))
} ?: ""
}
}
}
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import kotlinx.serialization.Serializable
@Serializable
data class KitsuSearchItemCover(
val original: String?,
)
@@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.data.track.kitsu.dto
import kotlinx.serialization.Serializable
@Serializable
data class KitsuCurrentUserResult(
val data: List<KitsuUser>,
)
@Serializable
data class KitsuUser(
val id: String,
)
@@ -6,8 +6,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.BaseTracker
import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.DeletableTracker
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@@ -106,7 +106,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
return track.copyFrom(series, rating) return track.copyFrom(series, rating)
} }
private fun Track.copyFrom(item: ListItem, rating: Rating?): Track = apply { private fun Track.copyFrom(item: MUListItem, rating: MURating?): Track = apply {
item.copyTo(this) item.copyTo(this)
score = rating?.rating ?: 0.0 score = rating?.rating ?: 0.0
} }
@@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.data.track.mangaupdates
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUContext
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MULoginResponse
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURecord
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUSearchResult
import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@@ -14,21 +16,15 @@ import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import logcat.LogPriority
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
@@ -38,20 +34,17 @@ class MangaUpdatesApi(
) { ) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val baseUrl = "https://api.mangaupdates.com"
private val contentType = "application/vnd.api+json".toMediaType()
private val authClient by lazy { private val authClient by lazy {
client.newBuilder() client.newBuilder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.build() .build()
} }
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> { suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
val listItem = with(json) { val listItem = with(json) {
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}")) authClient.newCall(GET("$BASE_URL/v1/lists/series/${track.remote_id}"))
.awaitSuccess() .awaitSuccess()
.parseAs<ListItem>() .parseAs<MUListItem>()
} }
val rating = getSeriesRating(track) val rating = getSeriesRating(track)
@@ -71,8 +64,8 @@ class MangaUpdatesApi(
} }
authClient.newCall( authClient.newCall(
POST( POST(
url = "$baseUrl/v1/lists/series", url = "$BASE_URL/v1/lists/series",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
@@ -98,8 +91,8 @@ class MangaUpdatesApi(
} }
authClient.newCall( authClient.newCall(
POST( POST(
url = "$baseUrl/v1/lists/series/update", url = "$BASE_URL/v1/lists/series/update",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
@@ -113,19 +106,19 @@ class MangaUpdatesApi(
} }
authClient.newCall( authClient.newCall(
POST( POST(
url = "$baseUrl/v1/lists/series/delete", url = "$BASE_URL/v1/lists/series/delete",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
} }
private suspend fun getSeriesRating(track: Track): Rating? { private suspend fun getSeriesRating(track: Track): MURating? {
return try { return try {
with(json) { with(json) {
authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating")) authClient.newCall(GET("$BASE_URL/v1/series/${track.remote_id}/rating"))
.awaitSuccess() .awaitSuccess()
.parseAs<Rating>() .parseAs<MURating>()
} }
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -140,22 +133,20 @@ class MangaUpdatesApi(
} }
authClient.newCall( authClient.newCall(
PUT( PUT(
url = "$baseUrl/v1/series/${track.remote_id}/rating", url = "$BASE_URL/v1/series/${track.remote_id}/rating",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
} else { } else {
authClient.newCall( authClient.newCall(
DELETE( DELETE(url = "$BASE_URL/v1/series/${track.remote_id}/rating"),
url = "$baseUrl/v1/series/${track.remote_id}/rating",
),
) )
.awaitSuccess() .awaitSuccess()
} }
} }
suspend fun search(query: String): List<Record> { suspend fun search(query: String): List<MURecord> {
val body = buildJsonObject { val body = buildJsonObject {
put("search", query) put("search", query)
put( put(
@@ -166,25 +157,22 @@ class MangaUpdatesApi(
}, },
) )
} }
return with(json) { return with(json) {
client.newCall( client.newCall(
POST( POST(
url = "$baseUrl/v1/series/search", url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<MUSearchResult>()
.let { obj -> .results
obj["results"]?.jsonArray?.map { element -> .map { it.record }
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
}
}
.orEmpty()
} }
} }
suspend fun authenticate(username: String, password: String): Context? { suspend fun authenticate(username: String, password: String): MUContext? {
val body = buildJsonObject { val body = buildJsonObject {
put("username", username) put("username", username)
put("password", password) put("password", password)
@@ -192,20 +180,19 @@ class MangaUpdatesApi(
return with(json) { return with(json) {
client.newCall( client.newCall(
PUT( PUT(
url = "$baseUrl/v1/account/login", url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(CONTENT_TYPE),
), ),
) )
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<MULoginResponse>()
.let { obj -> .context
try {
json.decodeFromJsonElement<Context>(obj["context"]!!)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
null
}
}
} }
} }
companion object {
private const val BASE_URL = "https://api.mangaupdates.com"
private val CONTENT_TYPE = "application/vnd.api+json".toMediaType()
}
} }
@@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Context( data class MUContext(
@SerialName("session_token") @SerialName("session_token")
val sessionToken: String, val sessionToken: String,
val uid: Long, val uid: Long,
@@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Image( data class MUImage(
val url: Url? = null, val url: MUUrl? = null,
val height: Int? = null, val height: Int? = null,
val width: Int? = null, val width: Int? = null,
) )

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