Compare commits

...

37 Commits

Author SHA1 Message Date
AntsyLich dea38912fc Use feature flags in compose compiler plugin
And slight cleanup

(cherry picked from commit 8f9a325895bb7b94c2ec92dd969094fc30b3b5e2)
2024-09-01 11:39:34 -04:00
renovate[bot] 5243346356 fix(deps): update dependency com.android.tools.build:gradle to v8.6.0 (#1178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f74071ab0a70c4fd649b451e58841539d011496a)
2024-09-01 11:39:27 -04:00
renovate[bot] 6bb3ec5b8a fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.1 (#1172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 7fb3ef48e4fafce471173111fe1632754e5e9e99)
2024-09-01 11:39:20 -04:00
renovate[bot] 7390e72045 fix(deps): update serialization.version to v1.7.2 (#1173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1837faa573f11a6b97fe13f358d6fa0e980c2ef7)
2024-09-01 11:39:12 -04:00
AntsyLich 64ff5cb9af Remove legacy broken source and history backup
(cherry picked from commit 518abf032ccb9bb45d197927be2a5faca4167d29)
2024-09-01 11:38:46 -04:00
Roshan Varughese 82f011e48e Hide keyboard when a Tracker SearchResultItem is clicked (#1168)
* Hide keyboard on select

* Code Review Suggestion

(cherry picked from commit 7ca64a67c5c64103aa3a5c7efb9227d3a98b715d)
2024-09-01 11:38:36 -04:00
renovate[bot] 8868a5db2b fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.0 (#1162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 607e56a4ec6393a3bfd25fe74cbae676fd94df22)
2024-09-01 11:38:27 -04:00
Catting a335feedfc Add "show entry" action to download notifications (#1159)
* Add 'show entry' to download notifications

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>

* fixup! Add 'show entry' to download notifications

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>

* fixup! Add 'show entry' to download notifications

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>

* spotless! Add 'show entry' to download notifications

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>

* Apply suggestions from code review

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

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

* fixup! spotless- Apply suggestions from code review

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>

---------

Signed-off-by: Catting <5874051+mm12@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 952a98c1804b2134e59fcb471c54cf7c4e1f415e)
2024-09-01 11:38:14 -04:00
Roshan Varughese 90ff5e69ec Add confirmation when adding repo via URI (#1158)
* Add confirmation when adding repo via URI

* Blank lines

* Suggestions

* Reverting Changes

* Removing Unused Imports

(cherry picked from commit 45628b14db477b266eb1f1f4ca9bec0b43f741cc)
2024-09-01 11:37:58 -04:00
Roshan Varughese 68a1820695 Respect privacy settings in extension update notification (#1156)
* Hide Extension Names in Update Notifications when Content is Hidden

* Moving `val` inside if

* [skip ci] Update CHANGELOG.md

(cherry picked from commit 5dc6569a683da47f5323c252fce1bd4094a5d232)

# Conflicts:
#	CHANGELOG.md
2024-09-01 11:37:48 -04:00
renovate[bot] 292b551027 fix(deps): update aboutlib.version to v11.2.3 (#1151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit fba9bacdc19dee7cdf9e3d1cb4ee4a496fa7b514)
2024-09-01 11:37:23 -04:00
Dani e19c62a8ae Add option to skip downloading duplicate read chapters (#1125)
* Add query to get chapter count by manga and chapter number

* Add functions to get chapter count by manga and chapter number

* Only count read chapters

* Add interactor

* Savepoint

* Extract new chapter logic to separate function

* Update javadocs

* Add preference to toggle new functionality

* Add todo

* Add debug logcat

* Use string resource instead of hardcoding title

* Add temporary logcat for debugging

* Fix detekt issues

* Update javadocs

* Update download unread chapters preference

* Remove debug logcat calls

* Update javadocs

* Resolve issue where read chapters were still being downloaded during manual manga fetch

* Apply code review changes

* Apply code review changes

* Revert "Apply code review changes"

This reverts commit 1a2dce78acc66a7c529ce5b572bdaf94804b1a30.

* Revert "Apply code review changes"

This reverts commit ac2a77829313967ad39ce3cb0c0231083b9d640d.

* Group download chapter logic inside the interactor GetChaptersToDownload

* Update javadocs

* Apply code review

* Apply code review

* Apply code review

* Update CHANGELOG.md to include the new feature

* Run spotless

* Update domain/src/main/java/mihon/domain/chapter/interactor/FilterChaptersForDownload.kt

---------

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

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2024-09-01 11:37:04 -04:00
renovate[bot] 348cb335c4 fix(deps): update moko to v0.24.2 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 379d5878266ba0287bfcc4a06452c27d70f33ba1)
2024-09-01 10:53:49 -04:00
renovate[bot] c426d11d76 chore(deps): update kotlin monorepo to v2.0.20 (#1144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 034ec4cb120c0f36cad1303de1314c28c4ec4969)
2024-09-01 10:53:39 -04:00
renovate[bot] 1965c0825d fix(deps): update dependency com.google.firebase:firebase-analytics to v22.1.0 (#1146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit ab2b734d49299dc3b30e0a7f0d5cbb268b0bff97)
2024-09-01 10:53:27 -04:00
renovate[bot] 0124763fcd fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.0 (#1142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 08ae51ea8c5ceccc8c5c65120f387d7b19d18052)
2024-09-01 10:53:18 -04:00
Hosted Weblate b0d737592c Translations update from Hosted Weblate
Co-authored-by: Ahmed seif al-nasr <ahmdsyfalnsr2@gmail.com>
Co-authored-by: Anas KANJO <anas.kanjo2022@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Frosted <cinardogan110@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gekka <1778962971@qq.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals
(cherry picked from commit 4387ae5ff3131dd4aaaacd75fa6e82e7b322d474)

# Conflicts:
#	i18n/src/commonMain/moko-resources/tr/strings.xml
2024-09-01 10:53:09 -04:00
renovate[bot] e14596465b fix(deps): update dependency org.conscrypt:conscrypt-android to v2.5.3 (#1135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit b2f1719c50365279e157a3b9ee015fc6c13a9a92)
2024-09-01 10:51:14 -04:00
renovate[bot] 5e52dfcc66 chore(deps): update dependency gradle to v8.10 (#1122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 3f050a83dd0907e0ffb56a1e1833f9de5b10b329)
2024-09-01 10:51:07 -04:00
renovate[bot] a09643fc77 fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.0 (#1121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 6f4e3f776f98d7a47dfa33b2cdfe992fc211ec28)
2024-09-01 10:51:00 -04:00
NGB-Was-Taken 19bc08659b Move "Choose what to sync" out of "Sync now" (#1264) 2024-09-01 10:45:30 -04:00
NGB-Was-Taken 2cb8f8f872 Show local chapters as downloaded on merged entries. (#1262)
* Show local chapters as downloaded on merged entries.

* Disable downloadIndicator for local chapters on merged entries.
2024-09-01 10:45:20 -04:00
NGB-Was-Taken 23c7bb09d3 Update links. (#1261)
* Update links.

* [skip ci] Change a tachiyomi.org link.
2024-09-01 10:45:11 -04:00
NGB-Was-Taken bdb8553e28 Respect thumbnailQuality and tryUsingFirstVolumeCover preferences. (MD) (#1260) 2024-09-01 10:45:01 -04:00
Weblate (bot) fbac29e0cd Translations update from Hosted Weblate (#1259)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Fadhil Muhammad <alpanumerik1@gmail.com>
Co-authored-by: HDYOU <308485965@qq.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: NGB-Was-Taken <myalternate34@gmail.com>
Co-authored-by: akir45 <akkn0708@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2024-09-01 10:44:49 -04:00
Jobobby04 b0aa2ffc42 Test to auto-add translations label 2024-08-23 16:39:17 -04:00
Jobobby04 45b5d9b8a4 Exclude weblate strings 2024-08-23 16:26:54 -04:00
Tran M. Cuong 91b98cdb82 Fix spotless error caused by #1253 being created before apply spotless (#1258) 2024-08-23 08:59:22 -04:00
Weblate (bot) 7f544f7163 Translations update from Hosted Weblate (#1253)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ja/
Translation: Mihon/TachiyomiSY

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: akir45 <akkn0708@gmail.com>
Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
2024-08-22 21:37:06 -04:00
Tran M. Cuong 3705880a77 Implement Mihon's spotless PR (#1257)
* Remove detekt (mihonapp/mihon#1130)

Annoying. More annoying in this project.

(cherry picked from commit 777ae2461e1eb277a3aa0c998ff69e4f100387a1)

* Add spotless (with ktlint) (mihonapp/mihon#1136)

(cherry picked from commit 5ae8095ef1ed2ae9f98486f9148e933c77a28692)

* Address spotless lint errors (mihonapp/mihon#1138)

* Add spotless (with ktlint)

* Run spotlessApply

* screaming case screaming case screaming case

* Update PagerViewerAdapter.kt

* Update ReaderTransitionView.kt

(cherry picked from commit d6252ab7703d52ecf9f43de3ee36fd63e665a31f)

* Generate locales_config.xml in build dir

(cherry picked from commit ac41bffdc97b4cfed923de6b9e8e01cccf3eb6eb)

* Address more spotless lint errors in SY

* some more missed

* more missed

* still missing, not sure while it won't report error when running locally

* one more

* more

* more

* correct comment

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-08-22 21:24:50 -04:00
Jobobby04 759fd4d4e3 Follow previous comment 2024-08-17 20:38:26 -04:00
FooIbar fc956fc791 Add comment about RecyclerView cache size (#1119)
Note for forks: Increasing cache size may cause OOM on API < 26, better
to make it API 26+ only.

(cherry picked from commit 1c47a6b9b35c622200c731cdbbc076f5263e8d06)
2024-08-17 20:33:30 -04:00
AntsyLich 9d7346157b Sync compose theme with MDC theme
(cherry picked from commit 9a34ace09c66274e6c2b3f9446058a0fa99d4bd0)

# Conflicts:
#	CHANGELOG.md
2024-08-17 20:31:20 -04:00
Weblate (bot) 0f0f4cf4a9 Translations update from Hosted Weblate (#1247)
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy-plurals/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/tachiyomisy/zh_Hant/
Translation: Mihon/TachiyomiSY
Translation: Mihon/TachiyomiSY Plurals

Co-authored-by: Ahmed seif al-nasr <ahmdsyfalnsr2@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Howard Wu <HowardWu20@outlook.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: TawfikSharaf <tawfikahmed132.wa@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: Tim Schneeberger <thebone.main@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2024-08-17 20:27:46 -04:00
akir45 426ef65102 Add japanese Translation (#1248)
* Add plurals.xml

* Add string.xml
2024-08-17 20:27:36 -04:00
Shamicen 95c834581b Libarchive refactor (#1249)
* Refactor archive support with libarchive

* Refactor archive support with libarchive

* Revert string resource changs

* Only mark archive formats as supported

Comic book archives should not be compressed.

* Fixup

* Remove epub from archive format list

* Move to mihon package

* Format

* Cleanup

Co-authored-by: Shamicen <84282253+Shamicen@users.noreply.github.com>
(cherry picked from commit 239c38982c4fd55d4d86b37fd9c3c51c3b47d098)

* handle incorrect passwords

* lint

* fixed broken encryption detection + small tweaks

* Add safeguard to prevent ArchiveInputStream from being closed twice (#967)

* fix: Add safeguard to prevent ArchiveInputStream from being closed twice

* detekt

* lint: Make detekt happy

---------

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

(cherry picked from commit e620665dda9eb5cc39f09e6087ea4f60a3cbe150)

* fixed ArchiveReaderMode CACHE_TO_DISK

* Added some missing SY --> comments

---------

Co-authored-by: FooIbar <118464521+fooibar@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com>
2024-08-17 20:25:25 -04:00
NGB-Was-Taken 71f2daf8f3 Delete duplicate downloaded chapters when they are automatically marked as read (#1252) 2024-08-17 20:24:29 -04:00
436 changed files with 4499 additions and 4659 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
- name: Build app - name: Build app
run: ./gradlew detekt assembleDevDebug run: ./gradlew spotlessCheck assembleDevDebug
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
# SY --> # SY -->
- name: Build app and run unit tests - name: Build app and run unit tests
run: ./gradlew detekt assembleStandardRelease testStandardReleaseUnitTest --stacktrace run: ./gradlew spotlessCheck assembleStandardRelease testStandardReleaseUnitTest --stacktrace
- name: Sign APK - name: Sign APK
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
+26
View File
@@ -0,0 +1,26 @@
name: Label PRs
on:
pull_request:
types: [opened]
jobs:
label_pr:
runs-on: ubuntu-latest
steps:
- name: Check PR and Add Label
uses: actions/github-script@v7
with:
script: |
const prAuthor = context.payload.pull_request.user.login;
if (prAuthor === 'weblate') {
const labels = ['Translations'];
await github.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labels
});
}
+7 -14
View File
@@ -1,4 +1,4 @@
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing). Looking to report an issue/bug or make a feature request? Please refer to the [README file](/README.md#issues-feature-requests-and-contributing).
--- ---
@@ -9,7 +9,7 @@ Thanks for your interest in contributing to Tachiyomi!
Pull requests are welcome! Pull requests are welcome!
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware. If you're interested in taking on [an open issue](https://github.com/jobobby04/TachiyomiSY/issues), please comment on it so others are aware.
You do not need to ask for permission nor an assignment. You do not need to ask for permission nor an assignment.
## Prerequisites ## Prerequisites
@@ -24,34 +24,27 @@ Before you start, please note that the ability to use following technologies is
- [Android Studio](https://developer.android.com/studio) - [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes. - Emulator or phone with developer options enabled to test changes.
## Linting
Run the `detekt` gradle task. If the build fails, a report of issues can be found in `app/build/reports/detekt/`. The report is availble in several formats and details each issue that needs attention.
## Getting help ## Getting help
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing. - Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing.
# Translations # Translations
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details. Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details.
# Forks # Forks
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE). Forks are allowed so long as they abide by [the project's LICENSE](/LICENSE).
When creating a fork, remember to: When creating a fork, remember to:
- To avoid confusion with the main app: - To avoid confusion with the main app:
- Change the app name - Change the app name
- Change the app icon - Change the app icon
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) - Change or disable the [app update checker](/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
- To avoid installation conflicts: - To avoid installation conflicts:
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) - Change the `applicationId` in [`build.gradle.kts`](/app/build.gradle.kts)
- To avoid having your data polluting the main app's analytics and crash report services:
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
### Supporting Cloud Sync - Google Drive Implementation ### Supporting Cloud Sync - Google Drive Implementation
+1 -1
View File
@@ -82,7 +82,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary> <details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).** 1. **Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://github.com/jobobby04/tachiyomisy/releases) and the already opened [issues](https://github.com/jobobby04/tachiyomisy/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/1195734228319617024.svg)](https://discord.gg/mihon) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/1195734228319617024.svg)](https://discord.gg/mihon)
</details> </details>
+3 -8
View File
@@ -21,7 +21,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
// shortcutHelper.setFilePath("./shortcuts.xml") // shortcutHelper.setFilePath("./shortcuts.xml")
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android { android {
namespace = "eu.kanade.tachiyomi" namespace = "eu.kanade.tachiyomi"
@@ -38,7 +38,7 @@ android {
buildConfigField("boolean", "INCLUDE_UPDATER", "false") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
ndk { ndk {
abiFilters += SUPPORTED_ABIS abiFilters += supportedAbis
} }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -47,7 +47,7 @@ android {
abi { abi {
isEnable = true isEnable = true
reset() reset()
include(*SUPPORTED_ABIS.toTypedArray()) include(*supportedAbis.toTypedArray())
isUniversalApk = true isUniversalApk = true
} }
} }
@@ -212,10 +212,6 @@ dependencies {
// Disk // Disk
implementation(libs.disklrucache) implementation(libs.disklrucache)
implementation(libs.unifile) implementation(libs.unifile)
implementation(libs.bundles.archive)
// SY -->
implementation(libs.zip4j)
// SY <--
// Preferences // Preferences
implementation(libs.preferencektx) implementation(libs.preferencektx)
@@ -247,7 +243,6 @@ dependencies {
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
-3
View File
@@ -127,9 +127,6 @@
# XmlUtil # XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keep public enum nl.adaptivity.xmlutil.EventType { *; }
# Apache Commons Compress
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
# Firebase # Firebase
-keep class com.google.firebase.installations.** { *; } -keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; }
@@ -24,6 +24,7 @@ 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 mihon.data.repository.ExtensionRepoRepositoryImpl import mihon.data.repository.ExtensionRepoRepositoryImpl
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
import mihon.domain.extensionrepo.interactor.GetExtensionRepo import mihon.domain.extensionrepo.interactor.GetExtensionRepo
@@ -152,6 +153,7 @@ class DomainModule : InjektModule {
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) } addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }
@@ -32,10 +32,11 @@ class GetEnabledSources(
) { a, b, c -> Triple(a, b, c) }, ) { a, b, c -> Triple(a, b, c) },
// SY <-- // SY <--
repository.getSources(), repository.getSources(),
) { pinnedSourceIds, ) {
(enabledLanguages, disabledSources, lastUsedSource), pinnedSourceIds,
(excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter), (enabledLanguages, disabledSources, lastUsedSource),
sources, (excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter),
sources,
-> ->
val sourcesAndCategories = sourcesInCategories.map { val sourcesAndCategories = sourcesInCategories.map {
@@ -13,7 +13,7 @@ class SyncPreferences(
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "") fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L) fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "") fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
fun syncInterval() = preferenceStore.getInt("sync_interval", 0) fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
fun syncService() = preferenceStore.getInt("sync_service", 0) fun syncService() = preferenceStore.getInt("sync_service", 0)
@@ -18,12 +18,20 @@ class UiPreferences(
fun themeMode() = preferenceStore.getEnum( fun themeMode() = preferenceStore.getEnum(
"pref_theme_mode_key", "pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ThemeMode.SYSTEM
} else {
ThemeMode.LIGHT
},
) )
fun appTheme() = preferenceStore.getEnum( fun appTheme() = preferenceStore.getEnum(
"pref_app_theme", "pref_app_theme",
if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT }, if (DeviceUtil.isDynamicColorAvailable) {
AppTheme.MONET
} else {
AppTheme.DEFAULT
},
) )
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
@@ -240,7 +240,7 @@ private fun DetailsHeader(
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName}) Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode}) Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
NSFW: ${extension.isNsfw} NSFW: ${extension.isNsfw}
""".trimIndent() """.trimIndent(),
) )
if (extension is Extension.Installed) { if (extension is Extension.Installed) {
@@ -251,7 +251,7 @@ private fun DetailsHeader(
Obsolete: ${extension.isObsolete} Obsolete: ${extension.isObsolete}
Shared: ${extension.isShared} Shared: ${extension.isShared}
Repository: ${extension.repoUrl} Repository: ${extension.repoUrl}
""".trimIndent() """.trimIndent(),
) )
} }
} }
@@ -219,7 +219,9 @@ private fun ExtensionContent(
when (it) { when (it) {
is Extension.Available -> onInstallExtension(it) is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> onOpenExtension(it) is Extension.Installed -> onOpenExtension(it)
is Extension.Untrusted -> { trustState = it } is Extension.Untrusted -> {
trustState = it
}
} }
}, },
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
@@ -241,7 +243,9 @@ private fun ExtensionContent(
onOpenExtension(it) onOpenExtension(it)
} }
} }
is Extension.Untrusted -> { trustState = it } is Extension.Untrusted -> {
trustState = it
}
} }
}, },
) )
@@ -35,7 +35,7 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -179,7 +179,7 @@ private fun SourcePinButton(
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
} else { } else {
MaterialTheme.colorScheme.onBackground.copy( MaterialTheme.colorScheme.onBackground.copy(
alpha = SecondaryItemAlpha, alpha = SECONDARY_ALPHA,
) )
} }
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin
@@ -79,7 +79,7 @@ fun TabbedDialog(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
state = pagerState, state = pagerState,
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
pageContent = { page -> content(page) } pageContent = { page -> content(page) },
) )
} }
} }
@@ -207,7 +207,6 @@ private fun ColumnScope.SortPage(
}.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty()) }.collectAsState(initial = screenModel.libraryPreferences.sortTagsForLibrary().get().isNotEmpty())
// SY <-- // SY <--
val trackerSortOption = if (trackers.isEmpty()) { val trackerSortOption = if (trackers.isEmpty()) {
emptyList() emptyList()
} else { } else {
@@ -62,7 +62,7 @@ private val ContinueReadingButtonIconSizeLarge = 20.dp
private val ContinueReadingButtonGridPadding = 6.dp private val ContinueReadingButtonGridPadding = 6.dp
private val ContinueReadingButtonListSpacing = 8.dp private val ContinueReadingButtonListSpacing = 8.dp
private const val GridSelectedCoverAlpha = 0.76f private const val GRID_SELECTED_COVER_ALPHA = 0.76f
/** /**
* Layout of grid list item with title overlaying the cover. * Layout of grid list item with title overlaying the cover.
@@ -90,7 +90,7 @@ fun MangaCompactGridItem(
MangaCover.Book( MangaCover.Book(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), .alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
data = coverData, data = coverData,
) )
}, },
@@ -197,7 +197,7 @@ fun MangaComfortableGridItem(
MangaCover.Book( MangaCover.Book(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), .alpha(if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha),
data = coverData, data = coverData,
) )
}, },
@@ -371,7 +371,7 @@ fun MangaListItem(
size = ContinueReadingButtonSizeSmall, size = ContinueReadingButtonSizeSmall,
iconSize = ContinueReadingButtonIconSizeSmall, iconSize = ContinueReadingButtonIconSizeSmall,
onClick = onClickContinueReading, onClick = onClickContinueReading,
modifier = Modifier.padding(start = ContinueReadingButtonListSpacing) modifier = Modifier.padding(start = ContinueReadingButtonListSpacing),
) )
} }
} }
@@ -392,7 +392,7 @@ private fun ContinueReadingButton(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f), containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer), contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
), ),
modifier = Modifier.size(size) modifier = Modifier.size(size),
) { ) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
@@ -77,6 +77,7 @@ import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import eu.kanade.tachiyomi.ui.manga.ChapterList import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
import eu.kanade.tachiyomi.ui.manga.PagePreviewState import eu.kanade.tachiyomi.ui.manga.PagePreviewState
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import exh.metadata.MetadataUtil import exh.metadata.MetadataUtil
@@ -578,6 +579,7 @@ private fun MangaScreenSmallImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
mergedData = state.mergedData,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@@ -880,6 +882,7 @@ fun MangaScreenLargeImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
mergedData = state.mergedData,
chapters = listItem, chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected }, isAnyChapterSelected = chapters.fastAny { it.selected },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@@ -944,6 +947,7 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems( private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
mergedData: MergedMangaData?,
chapters: List<ChapterList>, chapters: List<ChapterList>,
isAnyChapterSelected: Boolean, isAnyChapterSelected: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -995,7 +999,9 @@ private fun LazyListScope.sharedChapterItems(
// SY <-- // SY <--
}, },
readProgress = item.chapter.lastPageRead readProgress = item.chapter.lastPageRead
.takeIf { /* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L } .takeIf {
/* SY --> */(!item.chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0L
}
?.let { ?.let {
stringResource( stringResource(
MR.strings.chapter_progress, MR.strings.chapter_progress,
@@ -1011,7 +1017,8 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read, read = item.chapter.read,
bookmark = item.chapter.bookmark, bookmark = item.chapter.bookmark,
selected = item.selected, selected = item.selected,
downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(), downloadIndicatorEnabled =
!isAnyChapterSelected && !(mergedData?.manga?.get(item.chapter.mangaId) ?: manga).isLocal(),
downloadStateProvider = { item.downloadState }, downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress }, downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -60,6 +60,6 @@ private fun MissingChaptersWarning(count: Int) {
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error.copy(alpha = SecondaryItemAlpha), color = MaterialTheme.colorScheme.error.copy(alpha = SECONDARY_ALPHA),
) )
} }
@@ -40,8 +40,8 @@ import eu.kanade.tachiyomi.data.download.model.Download
import me.saket.swipe.SwipeableActionsBox import me.saket.swipe.SwipeableActionsBox
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
@@ -135,7 +135,7 @@ fun MangaChapterListItem(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height }, onTextLayout = { textHeight = it.size.height },
color = LocalContentColor.current.copy(alpha = if (read) ReadItemAlpha else 1f), color = LocalContentColor.current.copy(alpha = if (read) DISABLED_ALPHA else 1f),
) )
} }
@@ -143,7 +143,7 @@ fun MangaChapterListItem(
val subtitleStyle = MaterialTheme.typography.bodySmall val subtitleStyle = MaterialTheme.typography.bodySmall
.merge( .merge(
color = LocalContentColor.current color = LocalContentColor.current
.copy(alpha = if (read) ReadItemAlpha else SecondaryItemAlpha) .copy(alpha = if (read) DISABLED_ALPHA else SECONDARY_ALPHA),
) )
ProvideTextStyle(value = subtitleStyle) { ProvideTextStyle(value = subtitleStyle) {
if (date != null) { if (date != null) {
@@ -152,14 +152,19 @@ fun MangaChapterListItem(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (readProgress != null || scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText() if (readProgress != null ||
scanlator != null/* SY --> */ ||
sourceName != null/* SY <-- */
) {
DotSeparatorText()
}
} }
if (readProgress != null) { if (readProgress != null) {
Text( Text(
text = readProgress, text = readProgress,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha), color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
) )
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText() if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
} }
@@ -82,6 +82,7 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
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.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
@@ -181,7 +182,7 @@ fun MangaActionRow(
// SY <-- // SY <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = DISABLED_ALPHA)
// TODO: show something better when using custom interval // TODO: show something better when using custom interval
val nextUpdateDays = remember(nextUpdate) { val nextUpdateDays = remember(nextUpdate) {
@@ -44,7 +44,7 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
private fun PagePreviewLoading( private fun PagePreviewLoading(
setMaxWidth: (Dp) -> Unit setMaxWidth: (Dp) -> Unit,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
Box( Box(
@@ -63,7 +63,7 @@ private fun PagePreviewLoading(
@Composable @Composable
private fun PagePreviewRow( private fun PagePreviewRow(
onOpenPage: (Int) -> Unit, onOpenPage: (Int) -> Unit,
items: ImmutableList<PagePreview> items: ImmutableList<PagePreview>,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -88,7 +88,7 @@ private fun PagePreviewMore(
) { ) {
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center,
) { ) {
TextButton(onClick = onMorePreviewsClicked) { TextButton(onClick = onMorePreviewsClicked) {
Text(stringResource(SYMR.strings.more_previews)) Text(stringResource(SYMR.strings.more_previews))
@@ -116,7 +116,7 @@ fun PagePreviews(
pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach { pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
PagePreviewRow( PagePreviewRow(
onOpenPage = onOpenPage, onOpenPage = onOpenPage,
items = remember(it) { it.toImmutableList() } items = remember(it) { it.toImmutableList() },
) )
} }
@@ -153,7 +153,7 @@ fun LazyListScope.PagePreviewItems(
) { ) {
PagePreviewRow( PagePreviewRow(
onOpenPage = onOpenPage, onOpenPage = onOpenPage,
items = remember(it) { it.toImmutableList() } items = remember(it) { it.toImmutableList() },
) )
} }
item( item(
@@ -3,7 +3,6 @@ package eu.kanade.presentation.more.settings.screen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.webkit.WebStorage import android.webkit.WebStorage
import android.webkit.WebView import android.webkit.WebView
@@ -376,7 +375,7 @@ object SettingsAdvancedScreen : SearchableSettings {
chooseColorProfile.launch(arrayOf("*/*")) chooseColorProfile.launch(arrayOf("*/*"))
}, },
), ),
) ),
) )
} }
@@ -180,11 +180,15 @@ object SettingsAppearanceScreen : SearchableSettings {
Preference.PreferenceItem.SliderPreference( Preference.PreferenceItem.SliderPreference(
value = previewsRowCount, value = previewsRowCount,
title = stringResource(SYMR.strings.pref_previews_row_count), title = stringResource(SYMR.strings.pref_previews_row_count),
subtitle = if (previewsRowCount > 0) pluralStringResource( subtitle = if (previewsRowCount > 0) {
SYMR.plurals.row_count, pluralStringResource(
previewsRowCount, SYMR.plurals.row_count,
previewsRowCount, previewsRowCount,
) else stringResource(MR.strings.disabled), previewsRowCount,
)
} else {
stringResource(MR.strings.disabled)
},
min = 0, min = 0,
max = 10, max = 10,
onValueChanged = { onValueChanged = {
@@ -94,7 +94,7 @@ object SettingsBrowseScreen : SearchableSettings {
pref = uiPreferences.feedTabInFront(), pref = uiPreferences.feedTabInFront(),
title = stringResource(SYMR.strings.pref_feed_position), title = stringResource(SYMR.strings.pref_feed_position),
subtitle = stringResource(SYMR.strings.pref_feed_position_summery), subtitle = stringResource(SYMR.strings.pref_feed_position_summery),
enabled = hideFeedTab.not() enabled = hideFeedTab.not(),
), ),
), ),
), ),
@@ -394,11 +394,23 @@ object SettingsDataScreen : SearchableSettings {
syncServiceType: SyncManager.SyncService, syncServiceType: SyncManager.SyncService,
syncPreferences: SyncPreferences, syncPreferences: SyncPreferences,
): List<Preference> { ): List<Preference> {
return when (syncServiceType) { val navigator = LocalNavigator.currentOrThrow
val preferences = when (syncServiceType) {
SyncManager.SyncService.NONE -> emptyList() SyncManager.SyncService.NONE -> emptyList()
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences) SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences() SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
} }
return if (syncServiceType != SyncManager.SyncService.NONE) {
preferences + Preference.PreferenceItem.TextPreference(
title = stringResource(SYMR.strings.pref_choose_what_to_sync),
onClick = {
navigator.push(SyncSettingsSelector())
},
)
} else {
preferences
}
} }
@Composable @Composable
@@ -515,7 +527,7 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
private fun getSyncNowPref(): Preference.PreferenceGroup { private fun getSyncNowPref(): Preference.PreferenceGroup {
val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(SYMR.strings.pref_sync_now_group_title), title = stringResource(SYMR.strings.pref_sync_now_group_title),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
@@ -524,7 +536,11 @@ object SettingsDataScreen : SearchableSettings {
title = stringResource(SYMR.strings.pref_sync_now), title = stringResource(SYMR.strings.pref_sync_now),
subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle), subtitle = stringResource(SYMR.strings.pref_sync_now_subtitle),
onClick = { onClick = {
navigator.push(SyncSettingsSelector()) if (!SyncDataJob.isRunning(context)) {
SyncDataJob.startNow(context)
} else {
context.toast(SYMR.strings.sync_in_progress)
}
}, },
), ),
), ),
@@ -120,6 +120,7 @@ object SettingsDownloadScreen : SearchableSettings {
allCategories: List<Category>, allCategories: List<Category>,
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters() val downloadNewChaptersPref = downloadPreferences.downloadNewChapters()
val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly()
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories() val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories()
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude() val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude()
@@ -152,6 +153,11 @@ object SettingsDownloadScreen : SearchableSettings {
pref = downloadNewChaptersPref, pref = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new), title = stringResource(MR.strings.pref_download_new),
), ),
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters,
),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.categories), title = stringResource(MR.strings.categories),
subtitle = getCategoriesLabel( subtitle = getCategoriesLabel(
@@ -37,7 +37,7 @@ class OpenSourceLicensesScreen : Screen() {
name = it.name, name = it.name,
website = it.website, website = it.website,
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
) ),
) )
}, },
) )
@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConfirmDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
@@ -32,7 +33,7 @@ class ExtensionReposScreen(
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
LaunchedEffect(url) { LaunchedEffect(url) {
url?.let { screenModel.createRepo(it) } url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) }
} }
if (state is RepoScreenState.Loading) { if (state is RepoScreenState.Loading) {
@@ -67,7 +68,6 @@ class ExtensionReposScreen(
repo = dialog.repo, repo = dialog.repo,
) )
} }
is RepoDialog.Conflict -> { is RepoDialog.Conflict -> {
ExtensionRepoConflictDialog( ExtensionRepoConflictDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = screenModel::dismissDialog,
@@ -76,6 +76,13 @@ class ExtensionReposScreen(
newRepo = dialog.newRepo, newRepo = dialog.newRepo,
) )
} }
is RepoDialog.Confirm -> {
ExtensionRepoConfirmDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(dialog.url) },
repo = dialog.url,
)
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -125,6 +125,7 @@ sealed class RepoDialog {
data object Create : RepoDialog() data object Create : RepoDialog()
data class Delete(val repo: String) : RepoDialog() data class Delete(val repo: String) : RepoDialog()
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog() data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
data class Confirm(val url: String) : RepoDialog()
} }
sealed class RepoScreenState { sealed class RepoScreenState {
@@ -152,3 +152,35 @@ fun ExtensionRepoConflictDialog(
}, },
) )
} }
@Composable
fun ExtensionRepoConfirmDialog(
onDismissRequest: () -> Unit,
onCreate: () -> Unit,
repo: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.action_add_repo))
},
text = {
Text(text = stringResource(MR.strings.add_repo_confirmation, repo))
},
confirmButton = {
TextButton(
onClick = {
onCreate()
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_add))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
@@ -26,7 +26,6 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.SectionCard import tachiyomi.presentation.core.components.SectionCard
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@@ -16,7 +15,6 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.backup.create.BackupOptions import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -32,7 +30,6 @@ import uy.kohesive.injekt.api.get
class SyncSettingsSelector : Screen() { class SyncSettingsSelector : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { SyncSettingsSelectorModel() } val model = rememberScreenModel { SyncSettingsSelectorModel() }
val state by model.state.collectAsState() val state by model.state.collectAsState()
@@ -48,15 +45,10 @@ class SyncSettingsSelector : Screen() {
) { contentPadding -> ) { contentPadding ->
LazyColumnWithAction( LazyColumnWithAction(
contentPadding = contentPadding, contentPadding = contentPadding,
actionLabel = stringResource(SYMR.strings.label_sync), actionLabel = stringResource(MR.strings.action_save),
actionEnabled = state.options.canCreate(), actionEnabled = true,
onClickAction = { onClickAction = {
if (!SyncDataJob.isRunning(context)) { navigator.pop()
model.syncNow(context)
navigator.pop()
} else {
context.toast(SYMR.strings.sync_in_progress)
}
}, },
) { ) {
item { item {
@@ -28,7 +28,7 @@ import tachiyomi.presentation.core.i18n.stringResource
class BackupSchemaScreen : Screen() { class BackupSchemaScreen : Screen() {
companion object { companion object {
const val title = "Backup file schema" const val TITLE = "Backup file schema"
} }
@Composable @Composable
@@ -41,7 +41,7 @@ class BackupSchemaScreen : Screen() {
Scaffold( Scaffold(
topBar = { topBar = {
AppBar( AppBar(
title = title, title = TITLE,
navigateUp = navigator::pop, navigateUp = navigator::pop,
actions = { actions = {
AppBarActions( AppBarActions(
@@ -50,7 +50,7 @@ class BackupSchemaScreen : Screen() {
title = stringResource(MR.strings.action_copy_to_clipboard), title = stringResource(MR.strings.action_copy_to_clipboard),
icon = Icons.Default.ContentCopy, icon = Icons.Default.ContentCopy,
onClick = { onClick = {
context.copyToClipboard(title, schema) context.copyToClipboard(TITLE, schema)
}, },
), ),
), ),
@@ -31,11 +31,11 @@ class DebugInfoScreen : Screen() {
itemsProvider = { itemsProvider = {
listOf( listOf(
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = WorkerInfoScreen.title, title = WorkerInfoScreen.TITLE,
onClick = { navigator.push(WorkerInfoScreen()) }, onClick = { navigator.push(WorkerInfoScreen()) },
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = BackupSchemaScreen.title, title = BackupSchemaScreen.TITLE,
onClick = { navigator.push(BackupSchemaScreen()) }, onClick = { navigator.push(BackupSchemaScreen()) },
), ),
getAppInfoGroup(), getAppInfoGroup(),
@@ -49,7 +49,7 @@ import java.time.ZoneId
class WorkerInfoScreen : Screen() { class WorkerInfoScreen : Screen() {
companion object { companion object {
const val title = "Worker info" const val TITLE = "Worker info"
} }
@Composable @Composable
@@ -65,7 +65,7 @@ class WorkerInfoScreen : Screen() {
Scaffold( Scaffold(
topBar = { topBar = {
AppBar( AppBar(
title = title, title = TITLE,
navigateUp = navigator::pop, navigateUp = navigator::pop,
actions = { actions = {
AppBarActions( AppBarActions(
@@ -74,7 +74,7 @@ class WorkerInfoScreen : Screen() {
title = stringResource(MR.strings.action_copy_to_clipboard), title = stringResource(MR.strings.action_copy_to_clipboard),
icon = Icons.Default.ContentCopy, icon = Icons.Default.ContentCopy,
onClick = { onClick = {
context.copyToClipboard(title, enqueued + finished + running) context.copyToClipboard(TITLE, enqueued + finished + running)
}, },
), ),
), ),
@@ -159,7 +159,7 @@ class WorkerInfoScreen : Screen() {
Injekt.get<UiPreferences>().dateFormat().get(), Injekt.get<UiPreferences>().dateFormat().get(),
), ),
) )
appendLine("Next scheduled run: $timestamp",) appendLine("Next scheduled run: $timestamp")
appendLine("Attempt #${workInfo.runAttemptCount + 1}") appendLine("Attempt #${workInfo.runAttemptCount + 1}")
} }
appendLine() appendLine()
@@ -34,7 +34,9 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart import tachiyomi.presentation.core.util.isScrolledToStart
private enum class State { private enum class State {
CHECKED, INVERSED, UNCHECKED CHECKED,
INVERSED,
UNCHECKED,
} }
@Composable @Composable
@@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SECONDARY_ALPHA
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@Composable @Composable
@@ -73,7 +73,7 @@ private fun RowScope.BaseStatsItem(
style = subtitleStyle style = subtitleStyle
.copy( .copy(
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
.copy(alpha = SecondaryItemAlpha), .copy(alpha = SECONDARY_ALPHA),
), ),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
@@ -226,7 +226,7 @@ private fun ChapterText(
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
if (downloaded) { if (downloaded) {
appendInlineContent(DownloadedIconContentId) appendInlineContent(DOWNLOADED_ICON_ID)
append(' ') append(' ')
} }
append(name) append(name)
@@ -236,7 +236,7 @@ private fun ChapterText(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
inlineContent = persistentMapOf( inlineContent = persistentMapOf(
DownloadedIconContentId to InlineTextContent( DOWNLOADED_ICON_ID to InlineTextContent(
Placeholder( Placeholder(
width = 22.sp, width = 22.sp,
height = 22.sp, height = 22.sp,
@@ -273,7 +273,7 @@ private val CardColor: CardColors
) )
private val VerticalSpacerSize = 24.dp private val VerticalSpacerSize = 24.dp
private const val DownloadedIconContentId = "downloaded" private const val DOWNLOADED_ICON_ID = "downloaded"
private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy( private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy(
id = 0L, id = 0L,
@@ -63,7 +63,7 @@ fun ExhUtils(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.background(backgroundColor), .background(backgroundColor),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
AnimatedVisibility(visible = isVisible) { AnimatedVisibility(visible = isVisible) {
Column { Column {
@@ -84,7 +84,7 @@ fun ExhUtils(
) { ) {
Column( Column(
Modifier.weight(3f), Modifier.weight(3f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
text = stringResource(SYMR.strings.eh_autoscroll), text = stringResource(SYMR.strings.eh_autoscroll),
@@ -93,17 +93,17 @@ fun ExhUtils(
fontFamily = FontFamily.SansSerif, fontFamily = FontFamily.SansSerif,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = Modifier.fillMaxWidth(0.75f), modifier = Modifier.fillMaxWidth(0.75f),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
} }
Column( Column(
Modifier.weight(1f), Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Switch( Switch(
checked = isAutoScroll, checked = isAutoScroll,
onCheckedChange = null, onCheckedChange = null,
enabled = isAutoScrollEnabled enabled = isAutoScrollEnabled,
) )
} }
} }
@@ -114,7 +114,7 @@ fun ExhUtils(
) { ) {
Column( Column(
Modifier.weight(3f), Modifier.weight(3f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
var autoScrollFrequencyState by remember { var autoScrollFrequencyState by remember {
mutableStateOf(autoScrollFrequency) mutableStateOf(autoScrollFrequency)
@@ -38,7 +38,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.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
@@ -58,8 +57,6 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
private const val UnsetStatusTextAlpha = 0.5F
@Composable @Composable
fun TrackInfoDialogHome( fun TrackInfoDialogHome(
trackItems: List<TrackItem>, trackItems: List<TrackItem>,
@@ -211,10 +208,9 @@ private fun TrackInfoItem(
if (onScoreClick != null) { if (onScoreClick != null) {
VerticalDivider() VerticalDivider()
TrackDetailsItem( TrackDetailsItem(
modifier = Modifier modifier = Modifier.weight(1f),
.weight(1f) text = score,
.alpha(if (score == null) UnsetStatusTextAlpha else 1f), placeholder = stringResource(MR.strings.score),
text = score ?: stringResource(MR.strings.score),
onClick = onScoreClick, onClick = onScoreClick,
) )
} }
@@ -243,6 +239,8 @@ private fun TrackInfoItem(
} }
} }
private const val UNSET_TEXT_ALPHA = 0.5F
@Composable @Composable
private fun TrackDetailsItem( private fun TrackDetailsItem(
text: String?, text: String?,
@@ -263,7 +261,7 @@ private fun TrackDetailsItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UNSET_TEXT_ALPHA else 1f),
) )
} }
} }
@@ -224,6 +224,7 @@ private fun SearchResultItem(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current val clipboardManager: ClipboardManager = LocalClipboardManager.current
val focusManager = LocalFocusManager.current
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current) val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current) val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
val description = trackSearch.summary.trim() val description = trackSearch.summary.trim()
@@ -243,7 +244,10 @@ private fun SearchResultItem(
) )
.combinedClickable( .combinedClickable(
onLongClick = { dropDownMenuExpanded = true }, onLongClick = { dropDownMenuExpanded = true },
onClick = onClick, onClick = {
focusManager.clearFocus()
onClick()
},
) )
.padding(12.dp), .padding(12.dp),
) { ) {
@@ -43,7 +43,7 @@ import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
@@ -107,8 +107,10 @@ internal fun LazyListScope.updatesUiItems(
readProgress = updatesItem.update.lastPageRead readProgress = updatesItem.update.lastPageRead
.takeIf { .takeIf {
/* SY --> */( /* SY --> */(
!updatesItem.update.read || (preserveReadingPosition && updatesItem.isEhBasedUpdate()) !updatesItem.update.read ||
)/* SY <-- */ && it > 0L (preserveReadingPosition && updatesItem.isEhBasedUpdate())
)/* SY <-- */ &&
it > 0L
} }
?.let { ?.let {
stringResource( stringResource(
@@ -152,7 +154,7 @@ private fun UpdatesUiItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val textAlpha = if (update.read) ReadItemAlpha else 1f val textAlpha = if (update.read) DISABLED_ALPHA else 1f
Row( Row(
modifier = modifier modifier = modifier
@@ -226,7 +228,7 @@ private fun UpdatesUiItem(
Text( Text(
text = readProgress, text = readProgress,
maxLines = 1, maxLines = 1,
color = LocalContentColor.current.copy(alpha = ReadItemAlpha), color = LocalContentColor.current.copy(alpha = DISABLED_ALPHA),
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
@@ -1,6 +1,5 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
@@ -28,7 +27,6 @@ import soup.compose.material.motion.animation.rememberSlideDistance
/** /**
* For invoking back press to the parent activity * For invoking back press to the parent activity
*/ */
@SuppressLint("ComposeCompositionLocalUsage")
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
interface Tab : cafe.adriel.voyager.navigator.tab.Tab { interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
+2 -5
View File
@@ -174,8 +174,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
val syncPreferences: SyncPreferences = Injekt.get() val syncPreferences: SyncPreferences = Injekt.get()
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppStart) {
) {
SyncDataJob.startNow(this@App) SyncDataJob.startNow(this@App)
} }
@@ -199,7 +198,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
) )
} }
@Suppress("MagicNumber")
override fun newImageLoader(context: Context): ImageLoader { override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(this).apply { return ImageLoader.Builder(this).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client } val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
@@ -239,8 +237,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
val syncPreferences: SyncPreferences = Injekt.get() val syncPreferences: SyncPreferences = Injekt.get()
val syncTriggerOpt = syncPreferences.getSyncTriggerOptions() val syncTriggerOpt = syncPreferences.getSyncTriggerOptions()
if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume if (syncPreferences.isSyncEnabled() && syncTriggerOpt.syncOnAppResume) {
) {
SyncDataJob.startNow(this@App) SyncDataJob.startNow(this@App)
} }
} }
@@ -57,7 +57,7 @@ class BackupCreator(
// SY --> // SY -->
private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(), private val savedSearchBackupCreator: SavedSearchBackupCreator = SavedSearchBackupCreator(),
private val getMergedManga: GetMergedManga = Injekt.get(), private val getMergedManga: GetMergedManga = Injekt.get(),
private val handler: DatabaseHandler = Injekt.get() private val handler: DatabaseHandler = Injekt.get(),
// SY <-- // SY <--
) { ) {
@@ -92,7 +92,7 @@ class BackupCreator(
} else { } else {
emptyList() emptyList()
} + getMergedManga.await(), // SY <-- } + getMergedManga.await(), // SY <--
options options,
) )
val backup = Backup( val backup = Backup(
backupManga = backupManga, backupManga = backupManga,
@@ -39,7 +39,8 @@ data class BackupOptions(
// SY <-- // SY <--
) )
fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches fun canCreate() =
libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearches
companion object { companion object {
val libraryOptions = persistentListOf( val libraryOptions = persistentListOf(
@@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SavedSearchBackupCreator( class SavedSearchBackupCreator(
private val handler: DatabaseHandler = Injekt.get() private val handler: DatabaseHandler = Injekt.get(),
) { ) {
suspend operator fun invoke(): List<BackupSavedSearch> { suspend operator fun invoke(): List<BackupSavedSearch> {
@@ -3,12 +3,11 @@ package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@Suppress("MagicNumber")
@Serializable @Serializable
data class Backup( data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>, @ProtoNumber(1) val backupManga: List<BackupManga>,
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(), @ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(), // @ProtoNumber(100) var backupBrokenSources, legacy source model with non-compliant proto number,
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(), @ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(), @ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(), @ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
@@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@Suppress("MagicNumber")
@Serializable @Serializable
data class BackupChapter( data class BackupChapter(
// in 1.x some of these values have different names // in 1.x some of these values have different names
@@ -4,7 +4,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import mihon.domain.extensionrepo.model.ExtensionRepo import mihon.domain.extensionrepo.model.ExtensionRepo
@Suppress("MagicNumber")
@Serializable @Serializable
class BackupExtensionRepos( class BackupExtensionRepos(
@ProtoNumber(1) var baseUrl: String, @ProtoNumber(1) var baseUrl: String,
@@ -18,15 +18,3 @@ data class BackupHistory(
) )
} }
} }
@Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.")
@Serializable
data class BrokenBackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long,
@ProtoNumber(2) var readDuration: Long = 0,
) {
fun toBackupHistory(): BackupHistory {
return BackupHistory(url, lastRead, readDuration)
}
}
@@ -3,13 +3,9 @@ package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@Suppress( @Suppress("DEPRECATION")
"DEPRECATION",
"MagicNumber",
)
@Serializable @Serializable
data class BackupManga( data class BackupManga(
// in 1.x some of these values have different names // in 1.x some of these values have different names
@@ -36,7 +32,7 @@ data class BackupManga(
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
@ProtoNumber(100) var favorite: Boolean = true, @ProtoNumber(100) var favorite: Boolean = true,
@ProtoNumber(101) var chapterFlags: Int = 0, @ProtoNumber(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(), // @ProtoNumber(102) var brokenHistory, legacy history model with non-compliant proto number
@ProtoNumber(103) var viewer_flags: Int? = null, @ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(), @ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
@@ -36,7 +36,8 @@ data class BackupMergedMangaReference(
} }
val backupMergedMangaReferenceMapper = val backupMergedMangaReferenceMapper =
{ _: Long, {
_: Long,
isInfoManga: Boolean, isInfoManga: Boolean,
getChapterUpdates: Boolean, getChapterUpdates: Boolean,
chapterSortMode: Long, chapterSortMode: Long,
@@ -8,11 +8,3 @@ data class BackupSource(
@ProtoNumber(1) var name: String = "", @ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long, @ProtoNumber(2) var sourceId: Long,
) )
@Serializable
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long,
) {
fun toBackupSource() = BackupSource(name, sourceId)
}
@@ -53,7 +53,20 @@ data class BackupTracking(
} }
val backupTrackMapper = { val backupTrackMapper = {
_: Long, _: Long, syncId: Long, mediaId: Long, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, remoteUrl: String, startDate: Long, finishDate: Long -> _: Long,
_: Long,
syncId: Long,
mediaId: Long,
libraryId: Long?,
title: String,
lastChapterRead: Double,
totalChapters: Long,
status: Long,
score: Double,
remoteUrl: String,
startDate: Long,
finishDate: Long,
->
BackupTracking( BackupTracking(
syncId = syncId.toInt(), syncId = syncId.toInt(),
mediaId = mediaId, mediaId = mediaId,
@@ -74,7 +74,7 @@ class BackupRestorer(
val backup = BackupDecoder(context).decode(uri) val backup = BackupDecoder(context).decode(uri)
// Store source mapping for error messages // Store source mapping for error messages
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() } val backupMaps = backup.backupSources
sourceMapping = backupMaps.associate { it.sourceId to it.name } sourceMapping = backupMaps.associate { it.sourceId to it.name }
if (options.libraryEntries) { if (options.libraryEntries) {
@@ -200,7 +200,7 @@ class BackupRestorer(
} }
private fun CoroutineScope.restoreExtensionRepos( private fun CoroutineScope.restoreExtensionRepos(
backupExtensionRepo: List<BackupExtensionRepos> backupExtensionRepo: List<BackupExtensionRepos>,
) = launch { ) = launch {
backupExtensionRepo backupExtensionRepo
.forEach { .forEach {
@@ -27,7 +27,13 @@ data class RestoreOptions(
// SY <-- // SY <--
) )
fun canRestore() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings /* SY --> */ || savedSearches /* SY <-- */ fun canRestore() =
libraryEntries ||
categories ||
appSettings ||
extensionRepoSettings ||
sourceSettings /* SY --> */ ||
savedSearches /* SY <-- */
companion object { companion object {
val options = persistentListOf( val options = persistentListOf(
@@ -72,7 +78,7 @@ data class RestoreOptions(
extensionRepoSettings = array[3], extensionRepoSettings = array[3],
sourceSettings = array[4], sourceSettings = array[4],
// SY --> // SY -->
savedSearches = array[5] savedSearches = array[5],
// SY <-- // SY <--
) )
} }
@@ -8,7 +8,7 @@ import uy.kohesive.injekt.api.get
class ExtensionRepoRestorer( class ExtensionRepoRestorer(
private val handler: DatabaseHandler = Injekt.get(), private val handler: DatabaseHandler = Injekt.get(),
private val getExtensionRepos: GetExtensionRepo = Injekt.get() private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
@@ -32,7 +32,7 @@ class ExtensionRepoRestorer(
backupRepo.name, backupRepo.name,
backupRepo.shortName, backupRepo.shortName,
backupRepo.website, backupRepo.website,
backupRepo.signingKeyFingerprint backupRepo.signingKeyFingerprint,
) )
} }
} }
@@ -89,7 +89,7 @@ class MangaRestorer(
chapters = backupManga.chapters, chapters = backupManga.chapters,
categories = backupManga.categories, categories = backupManga.categories,
backupCategories = backupCategories, backupCategories = backupCategories,
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, history = backupManga.history,
tracks = backupManga.tracking, tracks = backupManga.tracking,
excludedScanlators = backupManga.excludedScanlators, excludedScanlators = backupManga.excludedScanlators,
// SY --> // SY -->
@@ -36,8 +36,8 @@ class ChapterCache(
private val context: Context, private val context: Context,
private val json: Json, private val json: Json,
// SY --> // SY -->
readerPreferences: ReaderPreferences readerPreferences: ReaderPreferences,
//S Y <-- // SY <--
) { ) {
// --> EH // --> EH
@@ -45,7 +45,6 @@ import java.io.IOException
* Available request parameter: * Available request parameter:
* - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true * - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true
*/ */
@Suppress("LongParameterList")
class MangaCoverFetcher( class MangaCoverFetcher(
private val url: String?, private val url: String?,
private val isLibraryManga: Boolean, private val isLibraryManga: Boolean,
@@ -86,7 +85,7 @@ class MangaCoverFetcher(
source = ImageSource( source = ImageSource(
file = file.toOkioPath(), file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM, fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey diskCacheKey = diskCacheKey,
), ),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
@@ -58,7 +58,7 @@ class PagePreviewFetcher(
source = ImageSource( source = ImageSource(
file = file.toOkioPath(), file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM, fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey diskCacheKey = diskCacheKey,
), ),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
@@ -230,7 +230,7 @@ class PagePreviewFetcher(
file = data, file = data,
fileSystem = FileSystem.SYSTEM, fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey, diskCacheKey = diskCacheKey,
closeable = this closeable = this,
) )
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil3.ImageLoader import coil3.ImageLoader
@@ -11,37 +12,37 @@ import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.request.bitmapConfig import coil3.request.bitmapConfig
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStream
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import net.lingala.zip4j.ZipFile import mihon.core.common.archive.archiveReader
import net.lingala.zip4j.model.FileHeader
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.BufferedInputStream
/** /**
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system. * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
*/ */
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder { class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
private val context = Injekt.get<Application>()
override suspend fun decode(): DecodeResult { override suspend fun decode(): DecodeResult {
// SY --> // SY -->
var zip4j: ZipFile? = null var coverStream: BufferedInputStream? = null
var entry: FileHeader? = null
if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) { if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) {
if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) { if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) {
zip4j = ZipFile(resources.file().toFile().absolutePath) coverStream = UniFile.fromFile(resources.file().toFile())
entry = zip4j.fileHeaders.firstOrNull { ?.archiveReader(context = context)
it.fileName.equals(CbzCrypto.DEFAULT_COVER_NAME, ignoreCase = true) ?.getCoverStream()
}
if (zip4j.isEncrypted) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
} }
} }
val decoder = resources.sourceOrNull()?.use { val decoder = resources.sourceOrNull()?.use {
zip4j.use { zipFile -> coverStream.use { coverStream ->
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile) ImageDecoder.newInstance(coverStream ?: it.inputStream(), options.cropBorders, displayProfile)
} }
} }
// SY <-- // SY <--
@@ -1,3 +1,5 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@@ -1,3 +1,5 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
class ChapterImpl : Chapter { class ChapterImpl : Chapter {
@@ -1,3 +1,5 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable import java.io.Serializable
@@ -1,3 +1,5 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
class TrackImpl : Track { class TrackImpl : Track {
@@ -83,6 +83,11 @@ internal class DownloadNotifier(private val context: Context) {
context.stringResource(MR.strings.action_pause), context.stringResource(MR.strings.action_pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context), NotificationReceiver.pauseDownloadsPendingBroadcast(context),
) )
addAction(
R.drawable.ic_book_24dp,
context.stringResource(MR.strings.action_show_manga),
NotificationReceiver.openEntryPendingActivity(context, download.manga.id),
)
} }
val downloadingProgressText = context.stringResource( val downloadingProgressText = context.stringResource(
@@ -160,9 +165,10 @@ internal class DownloadNotifier(private val context: Context) {
* *
* @param reason the text to show. * @param reason the text to show.
* @param timeout duration after which to automatically dismiss the notification. * @param timeout duration after which to automatically dismiss the notification.
* @param mangaId the id of the entry being warned about
* Only works on Android 8+. * Only works on Android 8+.
*/ */
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) { fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null, mangaId: Long? = null) {
with(errorNotificationBuilder) { with(errorNotificationBuilder) {
setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title)) setContentTitle(context.stringResource(MR.strings.download_notifier_downloader_title))
setStyle(NotificationCompat.BigTextStyle().bigText(reason)) setStyle(NotificationCompat.BigTextStyle().bigText(reason))
@@ -170,6 +176,13 @@ internal class DownloadNotifier(private val context: Context) {
setAutoCancel(true) setAutoCancel(true)
clearActions() clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
if (mangaId != null) {
addAction(
R.drawable.ic_book_24dp,
context.stringResource(MR.strings.action_show_manga),
NotificationReceiver.openEntryPendingActivity(context, mangaId),
)
}
setProgress(0, 0, false) setProgress(0, 0, false)
timeout?.let { setTimeoutAfter(it) } timeout?.let { setTimeoutAfter(it) }
contentIntent?.let { setContentIntent(it) } contentIntent?.let { setContentIntent(it) }
@@ -187,8 +200,9 @@ internal class DownloadNotifier(private val context: Context) {
* *
* @param error string containing error information. * @param error string containing error information.
* @param chapter string containing chapter title. * @param chapter string containing chapter title.
* @param mangaId the id of the entry that the error occurred on
*/ */
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) { fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null, mangaId: Long? = null) {
// Create notification // Create notification
with(errorNotificationBuilder) { with(errorNotificationBuilder) {
setContentTitle( setContentTitle(
@@ -198,6 +212,13 @@ internal class DownloadNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_warning_white_24dp) setSmallIcon(R.drawable.ic_warning_white_24dp)
clearActions() clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
if (mangaId != null) {
addAction(
R.drawable.ic_book_24dp,
context.stringResource(MR.strings.action_show_manga),
NotificationReceiver.openEntryPendingActivity(context, mangaId),
)
}
setProgress(0, 0, false) setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
@@ -121,7 +121,8 @@ class DownloadProvider(
getValidChapterDirNames(chp.name, chp.scanlator).any { dir -> getValidChapterDirNames(chp.name, chp.scanlator).any { dir ->
mangaDir.findFile(dir) != null mangaDir.findFile(dir) != null
} }
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true } == null ||
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} }
} }
// SY <-- // SY <--
@@ -44,10 +44,10 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import mihon.core.common.archive.ZipWriter
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.addFilesToZip
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNow import tachiyomi.core.common.util.lang.launchNow
@@ -65,12 +65,8 @@ import tachiyomi.domain.track.interactor.GetTracks
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
import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/** /**
* This class is the one in charge of downloading chapters. * This class is the one in charge of downloading chapters.
@@ -194,7 +190,7 @@ class Downloader(
fun clearQueue() { fun clearQueue() {
cancelDownloaderJob() cancelDownloaderJob()
_clearQueue() internalClearQueue()
notifier.dismissProgress() notifier.dismissProgress()
} }
@@ -208,9 +204,12 @@ class Downloader(
val activeDownloadsFlow = queueState.transformLatest { queue -> val activeDownloadsFlow = queueState.transformLatest { queue ->
while (true) { while (true) {
val activeDownloads = queue.asSequence() val activeDownloads = queue.asSequence()
.filter { it.status.value <= Download.State.DOWNLOADING.value } // Ignore completed downloads, leave them in the queue // Ignore completed downloads, leave them in the queue
.filter { it.status.value <= Download.State.DOWNLOADING.value }
.groupBy { it.source } .groupBy { it.source }
.toList().take(5) // Concurrently download from 5 different sources .toList()
// Concurrently download from 5 different sources
.take(5)
.map { (_, downloads) -> downloads.first() } .map { (_, downloads) -> downloads.first() }
emit(activeDownloads) emit(activeDownloads)
@@ -337,6 +336,7 @@ class Downloader(
context.stringResource(MR.strings.download_insufficient_space), context.stringResource(MR.strings.download_insufficient_space),
download.chapter.name, download.chapter.name,
download.manga.title, download.manga.title,
download.manga.id,
) )
return return
} }
@@ -426,7 +426,7 @@ class Downloader(
// If the page list threw, it will resume here // If the page list threw, it will resume here
logcat(LogPriority.ERROR, error) logcat(LogPriority.ERROR, error)
download.status = Download.State.ERROR download.status = Download.State.ERROR
notifier.onError(error.message, download.chapter.name, download.manga.title) notifier.onError(error.message, download.chapter.name, download.manga.title, download.manga.id)
} }
} }
@@ -475,7 +475,7 @@ class Downloader(
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
page.progress = 0 page.progress = 0
page.status = Page.State.ERROR page.status = Page.State.ERROR
notifier.onError(e.message, download.chapter.name, download.manga.title) notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
} }
} }
@@ -619,70 +619,19 @@ class Downloader(
tmpDir: UniFile, tmpDir: UniFile,
) { ) {
// SY --> // SY -->
if (CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet()) { val encrypt = CbzCrypto.getPasswordProtectDlPref() && CbzCrypto.isPasswordSet()
archiveEncryptedChapter(mangaDir, dirname, tmpDir)
return
}
// SY <-- // SY <--
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut -> ZipWriter(context, zip, /* SY --> */ encrypt /* SY <-- */).use { writer ->
zipOut.setMethod(ZipEntry.STORED) tmpDir.listFiles()?.forEach { file ->
writer.write(file)
tmpDir.listFiles()?.forEach { img ->
img.openInputStream().use { input ->
val data = input.readBytes()
val size = img.length()
val entry = ZipEntry(img.name).apply {
val crc = CRC32().apply {
update(data)
}
setCrc(crc.value)
compressedSize = size
setSize(size)
}
zipOut.putNextEntry(entry)
zipOut.write(data)
}
} }
} }
zip.renameTo("$dirname.cbz") zip.renameTo("$dirname.cbz")
tmpDir.delete() tmpDir.delete()
} }
// SY -->
private fun archiveEncryptedChapter(
mangaDir: UniFile,
dirname: String,
tmpDir: UniFile,
) {
tmpDir.filePath?.let { addPaddingToImage(File(it)) }
tmpDir.listFiles()?.toList()?.let { files ->
mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
}
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
tmpDir.delete()
}
private fun addPaddingToImage(imageDir: File) {
imageDir.listFiles()
// using ImageUtils isImage and findImageType functions causes IO errors when deleting files to set Exif Metadata
// it should be safe to assume that all files with image extensions are actual images at this point
?.filter {
it.extension.equals("jpg", true) ||
it.extension.equals("jpeg", true) ||
it.extension.equals("png", true) ||
it.extension.equals("webp", true)
}
?.forEach { ImageUtil.addPaddingToImageExif(it) }
}
// SY <--
/** /**
* Creates a ComicInfo.xml file inside the given directory. * Creates a ComicInfo.xml file inside the given directory.
*/ */
@@ -705,7 +654,7 @@ class Downloader(
chapter, chapter,
urls, urls,
categories, categories,
source.name source.name,
) )
// Remove the old file // Remove the old file
@@ -765,7 +714,7 @@ class Downloader(
removeFromQueueIf { it.manga.id == manga.id } removeFromQueueIf { it.manga.id == manga.id }
} }
private fun _clearQueue() { private fun internalClearQueue() {
_queueState.update { _queueState.update {
it.forEach { download -> it.forEach { download ->
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
@@ -787,7 +736,7 @@ class Downloader(
} }
pause() pause()
_clearQueue() internalClearQueue()
addAllToQueue(downloads) addAllToQueue(downloads)
if (wasRunning) { if (wasRunning) {
@@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
@@ -58,17 +57,16 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.NoChaptersException import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.model.GroupLibraryMode import tachiyomi.domain.library.model.GroupLibraryMode
import tachiyomi.domain.library.model.LibraryGroup import tachiyomi.domain.library.model.LibraryGroup
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
@@ -108,16 +106,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
private val downloadPreferences: DownloadPreferences = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get() private val libraryPreferences: LibraryPreferences = Injekt.get()
private val downloadManager: DownloadManager = Injekt.get() private val downloadManager: DownloadManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get() private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get() private val getLibraryManga: GetLibraryManga = Injekt.get()
private val getManga: GetManga = Injekt.get() private val getManga: GetManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get() private val updateManga: UpdateManga = Injekt.get()
private val getCategories: GetCategories = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get() private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
// SY --> // SY -->
private val getFavorites: GetFavorites = Injekt.get() private val getFavorites: GetFavorites = Injekt.get()
@@ -363,14 +360,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
async { async {
semaphore.withPermit { semaphore.withPermit {
if ( if (
mdlistLogged && mangaInSource.firstOrNull() mdlistLogged &&
mangaInSource.firstOrNull()
?.let { it.manga.source in mangaDexSourceIds } == true ?.let { it.manga.source in mangaDexSourceIds } == true
) { ) {
launch { launch {
mangaInSource.forEach { (manga) -> mangaInSource.forEach { (manga) ->
try { try {
val tracks = getTracks.await(manga.id) val tracks = getTracks.await(manga.id)
if (tracks.isEmpty() || tracks.none { it.trackerId == TrackerManager.MDLIST }) { if (tracks.isEmpty() ||
tracks.none { it.trackerId == TrackerManager.MDLIST }
) {
val track = mdList.createInitialTracker(manga) val track = mdList.createInitialTracker(manga)
insertTrack.await(mdList.refresh(track).toDomainTrack(false)!!) insertTrack.await(mdList.refresh(track).toDomainTrack(false)!!)
} }
@@ -400,10 +400,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
// SY --> // SY -->
.sortedByDescending { it.sourceOrder }.run { .sortedByDescending { it.sourceOrder }.run {
if (libraryPreferences.libraryReadDuplicateChapters().get()) { if (libraryPreferences.libraryReadDuplicateChapters().get()) {
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read } val readChapters = getChaptersByMangaId.await(manga.id).filter {
it.read
}
val newReadChapters = this.filter { chapter -> val newReadChapters = this.filter { chapter ->
chapter.chapterNumber > 0 && chapter.chapterNumber > 0 &&
readChapters.any { it.chapterNumber == chapter.chapterNumber } readChapters.any {
it.chapterNumber == chapter.chapterNumber
}
} }
if (newReadChapters.isNotEmpty()) { if (newReadChapters.isNotEmpty()) {
@@ -415,12 +419,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
this this
} }
} }
//SY <-- // SY <--
if (newChapters.isNotEmpty()) { if (newChapters.isNotEmpty()) {
val categoryIds = getCategories.await(manga.id).map { it.id } val chaptersToDownload = filterChaptersForDownload.await(manga, newChapters)
if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) {
downloadChapters(manga, newChapters) if (chaptersToDownload.isNotEmpty()) {
downloadChapters(manga, chaptersToDownload)
hasDownloads.set(true) hasDownloads.set(true)
} }
@@ -766,7 +771,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val constraints = Constraints( val constraints = Constraints(
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
NetworkType.UNMETERED NetworkType.UNMETERED
} else { NetworkType.CONNECTED }, } else {
NetworkType.CONNECTED
},
requiresCharging = DEVICE_CHARGING in restrictions, requiresCharging = DEVICE_CHARGING in restrictions,
requiresBatteryNotLow = true, requiresBatteryNotLow = true,
) )
@@ -265,6 +265,8 @@ class NotificationReceiver : BroadcastReceiver() {
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER" private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER" private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
private const val ACTION_OPEN_ENTRY = "$ID.$NAME.ACTION_OPEN_ENTRY"
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS" private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS" private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
@@ -503,6 +505,26 @@ class NotificationReceiver : BroadcastReceiver() {
) )
} }
/**
* Returns [PendingIntent] that opens the manga info controller
*
* @param context context of application
* @param mangaId id of the entry to open
*/
internal fun openEntryPendingActivity(context: Context, mangaId: Long): PendingIntent {
val newIntent = Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(Constants.MANGA_EXTRA, mangaId)
.putExtra("notificationId", mangaId.hashCode())
return PendingIntent.getActivity(
context,
mangaId.hashCode(),
newIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
/** /**
* Returns [PendingIntent] that starts a service which stops the library update * Returns [PendingIntent] that starts a service which stops the library update
* *
@@ -147,7 +147,7 @@ class SyncManager(
return return
} }
if (remoteBackup === syncData.backup){ if (remoteBackup === syncData.backup) {
// nothing changed // nothing changed
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" } logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
syncPreferences.lastSyncTimestamp().set(Date().time) syncPreferences.lastSyncTimestamp().set(Date().time)
@@ -72,7 +72,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
try { try {
val remoteSData = pullSyncData() val remoteSData = pullSyncData()
if (remoteSData != null ){ if (remoteSData != null) {
// Get local unique device ID // Get local unique device ID
val localDeviceId = syncPreferences.uniqueDeviceID() val localDeviceId = syncPreferences.uniqueDeviceID()
val lastSyncDeviceId = remoteSData.deviceId val lastSyncDeviceId = remoteSData.deviceId
@@ -86,7 +86,7 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
return if (lastSyncDeviceId == localDeviceId) { return if (lastSyncDeviceId == localDeviceId) {
pushSyncData(syncData) pushSyncData(syncData)
syncData.backup syncData.backup
}else{ } else {
// Merge the local and remote sync data // Merge the local and remote sync data
val mergedSyncData = mergeSyncData(syncData, remoteSData) val mergedSyncData = mergeSyncData(syncData, remoteSData)
pushSyncData(mergedSyncData) pushSyncData(mergedSyncData)
@@ -165,7 +165,9 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
appProperties = mapOf("deviceId" to syncData.deviceId) appProperties = mapOf("deviceId" to syncData.deviceId)
} }
drive.files().update(fileId, fileMetadata, mediaContent).execute() drive.files().update(fileId, fileMetadata, mediaContent).execute()
logcat(LogPriority.DEBUG) { "Updated existing sync data file in Google Drive with file ID: $fileId" } logcat(LogPriority.DEBUG) {
"Updated existing sync data file in Google Drive with file ID: $fileId"
}
} else { } else {
val fileMetadata = File().apply { val fileMetadata = File().apply {
name = remoteFileName name = remoteFileName
@@ -176,7 +178,9 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
val uploadedFile = drive.files().create(fileMetadata, mediaContent) val uploadedFile = drive.files().create(fileMetadata, mediaContent)
.setFields("id") .setFields("id")
.execute() .execute()
logcat(LogPriority.DEBUG) { "Created new sync data file in Google Drive with file ID: ${uploadedFile.id}" } logcat(LogPriority.DEBUG) {
"Created new sync data file in Google Drive with file ID: ${uploadedFile.id}"
}
} }
} }
} }
@@ -203,7 +207,6 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
} }
} }
suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus { suspend fun deleteSyncDataFromGoogleDrive(): DeleteSyncDataStatus {
val drive = googleDriveService.driveService val drive = googleDriveService.driveService
@@ -26,7 +26,7 @@ abstract class SyncService(
val json: Json, val json: Json,
val syncPreferences: SyncPreferences, val syncPreferences: SyncPreferences,
) { ) {
abstract suspend fun doSync(syncData: SyncData): Backup?; abstract suspend fun doSync(syncData: SyncData): Backup?
/** /**
* Merges the local and remote sync data into a single JSON string. * Merges the local and remote sync data into a single JSON string.
@@ -44,7 +44,8 @@ abstract class SyncService(
remoteSyncData.backup?.backupManga, remoteSyncData.backup?.backupManga,
localSyncData.backup?.backupCategories ?: emptyList(), localSyncData.backup?.backupCategories ?: emptyList(),
remoteSyncData.backup?.backupCategories ?: emptyList(), remoteSyncData.backup?.backupCategories ?: emptyList(),
mergedCategoriesList) mergedCategoriesList,
)
val mergedSourcesList = val mergedSourcesList =
mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources) mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources)
@@ -120,11 +121,13 @@ abstract class SyncService(
val mergedCategoriesMapByName = mergedCategories.associateBy { it.name } val mergedCategoriesMapByName = mergedCategories.associateBy { it.name }
fun updateCategories(theManga: BackupManga, theMap: Map<Long, BackupCategory>): BackupManga { fun updateCategories(theManga: BackupManga, theMap: Map<Long, BackupCategory>): BackupManga {
return theManga.copy(categories = theManga.categories.mapNotNull { return theManga.copy(
theMap[it]?.let { category -> categories = theManga.categories.mapNotNull {
mergedCategoriesMapByName[category.name]?.order theMap[it]?.let { category ->
} mergedCategoriesMapByName[category.name]?.order
}) }
},
)
} }
logcat(LogPriority.DEBUG, logTag) { logcat(LogPriority.DEBUG, logTag) {
@@ -147,7 +150,7 @@ abstract class SyncService(
} }
updateCategories( updateCategories(
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)), local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
localCategoriesMapByOrder localCategoriesMapByOrder,
) )
} else { } else {
logcat(LogPriority.DEBUG, logTag) { logcat(LogPriority.DEBUG, logTag) {
@@ -155,7 +158,7 @@ abstract class SyncService(
} }
updateCategories( updateCategories(
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)), remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
remoteCategoriesMapByOrder remoteCategoriesMapByOrder,
) )
} }
} }
@@ -301,7 +304,7 @@ abstract class SyncService(
private fun mergeSourcesLists( private fun mergeSourcesLists(
localSources: List<BackupSource>?, localSources: List<BackupSource>?,
remoteSources: List<BackupSource>? remoteSources: List<BackupSource>?,
): List<BackupSource> { ): List<BackupSource> {
val logTag = "MergeSources" val logTag = "MergeSources"
@@ -346,7 +349,7 @@ abstract class SyncService(
private fun mergePreferencesLists( private fun mergePreferencesLists(
localPreferences: List<BackupPreference>?, localPreferences: List<BackupPreference>?,
remotePreferences: List<BackupPreference>? remotePreferences: List<BackupPreference>?,
): List<BackupPreference> { ): List<BackupPreference> {
val logTag = "MergePreferences" val logTag = "MergePreferences"
@@ -394,7 +397,7 @@ abstract class SyncService(
private fun mergeSourcePreferencesLists( private fun mergeSourcePreferencesLists(
localPreferences: List<BackupSourcePreferences>?, localPreferences: List<BackupSourcePreferences>?,
remotePreferences: List<BackupSourcePreferences>? remotePreferences: List<BackupSourcePreferences>?,
): List<BackupSourcePreferences> { ): List<BackupSourcePreferences> {
val logTag = "MergeSourcePreferences" val logTag = "MergeSourcePreferences"
@@ -408,38 +411,39 @@ abstract class SyncService(
} }
// Merge both source preferences maps // Merge both source preferences maps
val mergedSourcePreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct().mapNotNull { sourceKey -> val mergedSourcePreferences = (localPreferencesMap.keys + remotePreferencesMap.keys).distinct()
val localSourcePreference = localPreferencesMap[sourceKey] .mapNotNull { sourceKey ->
val remoteSourcePreference = remotePreferencesMap[sourceKey] val localSourcePreference = localPreferencesMap[sourceKey]
val remoteSourcePreference = remotePreferencesMap[sourceKey]
logcat(LogPriority.DEBUG, logTag) { logcat(LogPriority.DEBUG, logTag) {
"Processing source preference key: $sourceKey. " + "Processing source preference key: $sourceKey. " +
"Local source preference: ${localSourcePreference != null}, " + "Local source preference: ${localSourcePreference != null}, " +
"Remote source preference: ${remoteSourcePreference != null}" "Remote source preference: ${remoteSourcePreference != null}"
} }
when { when {
localSourcePreference != null && remoteSourcePreference == null -> { localSourcePreference != null && remoteSourcePreference == null -> {
logcat(LogPriority.DEBUG, logTag) { logcat(LogPriority.DEBUG, logTag) {
"Using local source preference: ${localSourcePreference.sourceKey}." "Using local source preference: ${localSourcePreference.sourceKey}."
}
localSourcePreference
} }
localSourcePreference remoteSourcePreference != null && localSourcePreference == null -> {
} logcat(LogPriority.DEBUG, logTag) {
remoteSourcePreference != null && localSourcePreference == null -> { "Using remote source preference: ${remoteSourcePreference.sourceKey}."
logcat(LogPriority.DEBUG, logTag) { }
"Using remote source preference: ${remoteSourcePreference.sourceKey}." remoteSourcePreference
} }
remoteSourcePreference localSourcePreference != null && remoteSourcePreference != null -> {
// Merge the individual preferences within the source preferences
val mergedPrefs =
mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs)
BackupSourcePreferences(sourceKey, mergedPrefs)
}
else -> null
} }
localSourcePreference != null && remoteSourcePreference != null -> {
// Merge the individual preferences within the source preferences
val mergedPrefs =
mergeIndividualPreferences(localSourcePreference.prefs, remoteSourcePreference.prefs)
BackupSourcePreferences(sourceKey, mergedPrefs)
}
else -> null
} }
}
logcat(LogPriority.DEBUG, logTag) { logcat(LogPriority.DEBUG, logTag) {
"Source preferences merge completed. Total merged source preferences: ${mergedSourcePreferences.size}" "Source preferences merge completed. Total merged source preferences: ${mergedSourcePreferences.size}"
@@ -450,7 +454,7 @@ abstract class SyncService(
private fun mergeIndividualPreferences( private fun mergeIndividualPreferences(
localPrefs: List<BackupPreference>, localPrefs: List<BackupPreference>,
remotePrefs: List<BackupPreference> remotePrefs: List<BackupPreference>,
): List<BackupPreference> { ): List<BackupPreference> {
val mergedPrefsMap = (localPrefs + remotePrefs).associateBy { it.key } val mergedPrefsMap = (localPrefs + remotePrefs).associateBy { it.key }
return mergedPrefsMap.values.toList() return mergedPrefsMap.values.toList()
@@ -459,7 +463,7 @@ abstract class SyncService(
// SY --> // SY -->
private fun mergeSavedSearchesLists( private fun mergeSavedSearchesLists(
localSearches: List<BackupSavedSearch>?, localSearches: List<BackupSavedSearch>?,
remoteSearches: List<BackupSavedSearch>? remoteSearches: List<BackupSavedSearch>?,
): List<BackupSavedSearch> { ): List<BackupSavedSearch> {
val logTag = "MergeSavedSearches" val logTag = "MergeSavedSearches"
@@ -38,7 +38,7 @@ class SyncYomiSyncService(
try { try {
val (remoteData, etag) = pullSyncData() val (remoteData, etag) = pullSyncData()
val finalSyncData = if (remoteData != null){ val finalSyncData = if (remoteData != null) {
assert(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" } assert(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
logcat(LogPriority.DEBUG, "SyncService") { logcat(LogPriority.DEBUG, "SyncService") {
"Try update remote data with ETag($etag)" "Try update remote data with ETag($etag)"
@@ -54,7 +54,6 @@ class SyncYomiSyncService(
pushSyncData(finalSyncData, etag) pushSyncData(finalSyncData, etag)
return finalSyncData.backup return finalSyncData.backup
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" } logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
notifier.showSyncError(e.message) notifier.showSyncError(e.message)
@@ -113,7 +112,6 @@ class SyncYomiSyncService(
// return default value so we can overwrite it // return default value so we can overwrite it
Pair(null, "") Pair(null, "")
} }
} else { } else {
val responseBody = response.body.string() val responseBody = response.body.string()
notifier.showSyncError("Failed to download sync data: $responseBody") notifier.showSyncError("Failed to download sync data: $responseBody")
@@ -165,11 +163,9 @@ class SyncYomiSyncService(
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag") .takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
syncPreferences.lastSyncEtag().set(newETag) syncPreferences.lastSyncEtag().set(newETag)
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" } logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) { } else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
// other clients updated remote data, will try next time // other clients updated remote data, will try next time
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" } logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
} else { } else {
val responseBody = response.body.string() val responseBody = response.body.string()
notifier.showSyncError("Failed to upload sync data: $responseBody") notifier.showSyncError("Failed to upload sync data: $responseBody")
@@ -65,7 +65,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
with(json) { with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, API_URL,
body = payload.toString().toRequestBody(jsonMime), body = payload.toString().toRequestBody(jsonMime),
), ),
) )
@@ -109,7 +109,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
} }
} }
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
track track
} }
@@ -131,7 +131,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("listId", track.libraryId) put("listId", track.libraryId)
} }
} }
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess() .awaitSuccess()
} }
} }
@@ -172,7 +172,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
with(json) { with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, API_URL,
body = payload.toString().toRequestBody(jsonMime), body = payload.toString().toRequestBody(jsonMime),
), ),
) )
@@ -242,7 +242,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
with(json) { with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, API_URL,
body = payload.toString().toRequestBody(jsonMime), body = payload.toString().toRequestBody(jsonMime),
), ),
) )
@@ -286,7 +286,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
with(json) { with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, API_URL,
body = payload.toString().toRequestBody(jsonMime), body = payload.toString().toRequestBody(jsonMime),
), ),
) )
@@ -364,17 +364,17 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
companion object { companion object {
private const val clientId = "16329" private const val CLIENT_ID = "16329"
private const val apiUrl = "https://graphql.anilist.co/" private const val API_URL = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/" private const val BASE_URL = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/" private const val BASE_MANGA_URL = "https://anilist.co/manga/"
fun mangaUrl(mediaId: Long): String { fun mangaUrl(mediaId: Long): String {
return baseMangaUrl + mediaId return BASE_MANGA_URL + mediaId
} }
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon() fun authUrl(): Uri = "${BASE_URL}oauth/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()
} }
@@ -42,7 +42,7 @@ class BangumiApi(
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body)) authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
.awaitSuccess() .awaitSuccess()
track track
} }
@@ -55,7 +55,7 @@ class BangumiApi(
.add("rating", track.score.toInt().toString()) .add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody)) authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
.awaitSuccess() .awaitSuccess()
// chapter update // chapter update
@@ -64,7 +64,7 @@ class BangumiApi(
.build() .build()
authClient.newCall( authClient.newCall(
POST( POST(
"$apiUrl/subject/${track.remote_id}/update/watched_eps", "$API_URL/subject/${track.remote_id}/update/watched_eps",
body = body, body = body,
), ),
).awaitSuccess() ).awaitSuccess()
@@ -75,7 +75,7 @@ class BangumiApi(
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
.toUri() .toUri()
.buildUpon() .buildUpon()
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
@@ -124,7 +124,7 @@ class BangumiApi(
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
with(json) { with(json) {
authClient.newCall(GET("$apiUrl/subject/${track.remote_id}")) authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { jsonToSearch(it) } .let { jsonToSearch(it) }
@@ -134,7 +134,7 @@ class BangumiApi(
suspend fun statusLibManga(track: Track): Track? { suspend fun statusLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
val urlUserRead = "$apiUrl/collection/${track.remote_id}" val urlUserRead = "$API_URL/collection/${track.remote_id}"
val requestUserRead = Request.Builder() val requestUserRead = Request.Builder()
.url(urlUserRead) .url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK) .cacheControl(CacheControl.FORCE_NETWORK)
@@ -171,41 +171,41 @@ class BangumiApi(
} }
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
oauthUrl, OAUTH_URL,
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.add("code", code) .add("code", code)
.add("redirect_uri", redirectUrl) .add("redirect_uri", REDIRECT_URL)
.build(), .build(),
) )
companion object { companion object {
private const val clientId = "bgm291665acbd06a4c28" private const val CLIENT_ID = "bgm291665acbd06a4c28"
private const val clientSecret = "43e5ce36b207de16e5d3cfd3e79118db" private const val CLIENT_SECRET = "43e5ce36b207de16e5d3cfd3e79118db"
private const val apiUrl = "https://api.bgm.tv" private const val API_URL = "https://api.bgm.tv"
private const val oauthUrl = "https://bgm.tv/oauth/access_token" private const val OAUTH_URL = "https://bgm.tv/oauth/access_token"
private const val loginUrl = "https://bgm.tv/oauth/authorize" private const val LOGIN_URL = "https://bgm.tv/oauth/authorize"
private const val redirectUrl = "mihon://bangumi-auth" private const val REDIRECT_URL = "mihon://bangumi-auth"
fun authUrl(): Uri = fun authUrl(): Uri =
loginUrl.toUri().buildUpon() LOGIN_URL.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", REDIRECT_URL)
.build() .build()
fun refreshTokenRequest(token: String) = POST( fun refreshTokenRequest(token: String) = POST(
oauthUrl, OAUTH_URL,
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.add("refresh_token", token) .add("refresh_token", token)
.add("redirect_uri", redirectUrl) .add("redirect_uri", REDIRECT_URL)
.build(), .build(),
) )
} }
@@ -66,7 +66,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) { with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
"${baseUrl}library-entries", "${BASE_URL}library-entries",
headers = headersOf( headers = headersOf(
"Content-Type", "Content-Type",
"application/vnd.api+json", "application/vnd.api+json",
@@ -104,7 +104,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) { with(json) {
authClient.newCall( authClient.newCall(
Request.Builder() Request.Builder()
.url("${baseUrl}library-entries/${track.remote_id}") .url("${BASE_URL}library-entries/${track.remote_id}")
.headers( .headers(
headersOf( headersOf(
"Content-Type", "Content-Type",
@@ -130,7 +130,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
authClient authClient
.newCall( .newCall(
DELETE( DELETE(
"${baseUrl}library-entries/${track.remoteId}", "${BASE_URL}library-entries/${track.remoteId}",
headers = headersOf( headers = headersOf(
"Content-Type", "Content-Type",
"application/vnd.api+json", "application/vnd.api+json",
@@ -143,7 +143,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
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(algoliaKeyUrl)) authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { .let {
@@ -157,16 +157,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> { private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val jsonObject = buildJsonObject { val jsonObject = buildJsonObject {
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter") put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$ALGOLIA_FILTER")
} }
with(json) { with(json) {
client.newCall( client.newCall(
POST( POST(
algoliaUrl, ALGOLIA_URL,
headers = headersOf( headers = headersOf(
"X-Algolia-Application-Id", "X-Algolia-Application-Id",
algoliaAppId, ALGOLIA_APP_ID,
"X-Algolia-API-Key", "X-Algolia-API-Key",
key, key,
), ),
@@ -187,7 +187,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun findLibManga(track: Track, userId: String): Track? { suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext { return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon() val url = "${BASE_URL}library-entries".toUri().buildUpon()
.encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId") .encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
@@ -210,7 +210,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun getLibManga(track: Track): Track { suspend fun getLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon() val url = "${BASE_URL}library-entries".toUri().buildUpon()
.encodedQuery("filter[id]=${track.remote_id}") .encodedQuery("filter[id]=${track.remote_id}")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
@@ -237,11 +237,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("username", username) .add("username", username)
.add("password", password) .add("password", password)
.add("grant_type", "password") .add("grant_type", "password")
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.build() .build()
with(json) { with(json) {
client.newCall(POST(loginUrl, body = formBody)) client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
@@ -250,7 +250,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
val url = "${baseUrl}users".toUri().buildUpon() val url = "${BASE_URL}users".toUri().buildUpon()
.encodedQuery("filter[self]=true") .encodedQuery("filter[self]=true")
.build() .build()
with(json) { with(json) {
@@ -265,35 +265,31 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
companion object { companion object {
private const val clientId = private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val clientSecret =
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val baseUrl = "https://kitsu.app/api/edge/" private const val BASE_URL = "https://kitsu.app/api/edge/"
private const val loginUrl = "https://kitsu.app/api/oauth/token" private const val LOGIN_URL = "https://kitsu.app/api/oauth/token"
private const val baseMangaUrl = "https://kitsu.app/manga/" private const val BASE_MANGA_URL = "https://kitsu.app/manga/"
private const val algoliaKeyUrl = "https://kitsu.app/api/edge/algolia-keys/media/" private const val ALGOLIA_KEY_URL = "https://kitsu.app/api/edge/algolia-keys/media/"
private const val algoliaUrl = private const val ALGOLIA_APP_ID = "AWQO5J657S"
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/" private const val ALGOLIA_URL = "https://$ALGOLIA_APP_ID-dsn.algolia.net/1/indexes/production_media/query/"
private const val algoliaAppId = "AWQO5J657S" private const val ALGOLIA_FILTER = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" +
private const val algoliaFilter = "%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=" + "posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
"%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"
fun mangaUrl(remoteId: Long): String { fun mangaUrl(remoteId: Long): String {
return baseMangaUrl + remoteId return BASE_MANGA_URL + remoteId
} }
fun refreshTokenRequest(token: String) = POST( fun refreshTokenRequest(token: String) = POST(
loginUrl, LOGIN_URL,
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.add("refresh_token", token) .add("refresh_token", token)
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.build(), .build(),
) )
} }
@@ -6,8 +6,8 @@ import java.util.Locale
object KitsuDateHelper { object KitsuDateHelper {
private const val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" private const val PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
private val formatter = SimpleDateFormat(pattern, Locale.ENGLISH) private val formatter = SimpleDateFormat(PATTERN, Locale.ENGLISH)
fun convert(dateValue: Long): String? { fun convert(dateValue: Long): String? {
if (dateValue == 0L) return null if (dateValue == 0L) return null
@@ -1,3 +1,5 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming")
package eu.kanade.tachiyomi.data.track.model package eu.kanade.tachiyomi.data.track.model
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
@@ -54,7 +54,7 @@ class ShikimoriApi(
} }
authClient.newCall( authClient.newCall(
POST( POST(
"$apiUrl/v2/user_rates", "$API_URL/v2/user_rates",
body = payload.toString().toRequestBody(jsonMime), body = payload.toString().toRequestBody(jsonMime),
), ),
).awaitSuccess() ).awaitSuccess()
@@ -72,14 +72,14 @@ class ShikimoriApi(
suspend fun deleteLibManga(track: DomainTrack) { suspend fun deleteLibManga(track: DomainTrack) {
withIOContext { withIOContext {
authClient authClient
.newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}")) .newCall(DELETE("$API_URL/v2/user_rates/${track.libraryId}"))
.awaitSuccess() .awaitSuccess()
} }
} }
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val url = "$apiUrl/mangas".toUri().buildUpon() val url = "$API_URL/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity") .appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
@@ -102,10 +102,10 @@ class ShikimoriApi(
remote_id = obj["id"]!!.jsonPrimitive.long remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name"]!!.jsonPrimitive.content title = obj["name"]!!.jsonPrimitive.content
total_chapters = obj["chapters"]!!.jsonPrimitive.long total_chapters = obj["chapters"]!!.jsonPrimitive.long
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content cover_url = BASE_URL + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
summary = "" summary = ""
score = obj["score"]!!.jsonPrimitive.double score = obj["score"]!!.jsonPrimitive.double
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content tracking_url = BASE_URL + obj["url"]!!.jsonPrimitive.content
publishing_status = obj["status"]!!.jsonPrimitive.content publishing_status = obj["status"]!!.jsonPrimitive.content
publishing_type = obj["kind"]!!.jsonPrimitive.content publishing_type = obj["kind"]!!.jsonPrimitive.content
start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: "" start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: ""
@@ -121,13 +121,13 @@ class ShikimoriApi(
last_chapter_read = obj["chapters"]!!.jsonPrimitive.double last_chapter_read = obj["chapters"]!!.jsonPrimitive.double
score = obj["score"]!!.jsonPrimitive.int.toDouble() score = obj["score"]!!.jsonPrimitive.int.toDouble()
status = toTrackStatus(obj["status"]!!.jsonPrimitive.content) status = toTrackStatus(obj["status"]!!.jsonPrimitive.content)
tracking_url = baseUrl + mangas["url"]!!.jsonPrimitive.content tracking_url = BASE_URL + mangas["url"]!!.jsonPrimitive.content
} }
} }
suspend fun findLibManga(track: Track, userId: String): Track? { suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext { return withIOContext {
val urlMangas = "$apiUrl/mangas".toUri().buildUpon() val urlMangas = "$API_URL/mangas".toUri().buildUpon()
.appendPath(track.remote_id.toString()) .appendPath(track.remote_id.toString())
.build() .build()
val mangas = with(json) { val mangas = with(json) {
@@ -136,7 +136,7 @@ class ShikimoriApi(
.parseAs<JsonObject>() .parseAs<JsonObject>()
} }
val url = "$apiUrl/v2/user_rates".toUri().buildUpon() val url = "$API_URL/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", userId) .appendQueryParameter("user_id", userId)
.appendQueryParameter("target_id", track.remote_id.toString()) .appendQueryParameter("target_id", track.remote_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
@@ -160,7 +160,7 @@ class ShikimoriApi(
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): Int {
return with(json) { return with(json) {
authClient.newCall(GET("$apiUrl/users/whoami")) authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { .let {
@@ -180,39 +180,39 @@ class ShikimoriApi(
} }
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
oauthUrl, OAUTH_URL,
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.add("code", code) .add("code", code)
.add("redirect_uri", redirectUrl) .add("redirect_uri", REDIRECT_URL)
.build(), .build(),
) )
companion object { companion object {
private const val clientId = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA" private const val CLIENT_ID = "PB9dq8DzI405s7wdtwTdirYqHiyVMh--djnP7lBUqSA"
private const val clientSecret = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus" private const val CLIENT_SECRET = "NajpZcOBKB9sJtgNcejf8OB9jBN1OYYoo-k4h2WWZus"
private const val baseUrl = "https://shikimori.one" private const val BASE_URL = "https://shikimori.one"
private const val apiUrl = "$baseUrl/api" private const val API_URL = "$BASE_URL/api"
private const val oauthUrl = "$baseUrl/oauth/token" private const val OAUTH_URL = "$BASE_URL/oauth/token"
private const val loginUrl = "$baseUrl/oauth/authorize" private const val LOGIN_URL = "$BASE_URL/oauth/authorize"
private const val redirectUrl = "mihon://shikimori-auth" private const val REDIRECT_URL = "mihon://shikimori-auth"
fun authUrl(): Uri = loginUrl.toUri().buildUpon() fun authUrl(): Uri = LOGIN_URL.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", REDIRECT_URL)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.build() .build()
fun refreshTokenRequest(token: String) = POST( fun refreshTokenRequest(token: String) = POST(
oauthUrl, OAUTH_URL,
body = FormBody.Builder() body = FormBody.Builder()
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.add("client_id", clientId) .add("client_id", CLIENT_ID)
.add("client_secret", clientSecret) .add("client_secret", CLIENT_SECRET)
.add("refresh_token", token) .add("refresh_token", token)
.build(), .build(),
) )
@@ -69,17 +69,18 @@ class ExtensionManager(
private val iconMap = mutableMapOf<String, Drawable>() private val iconMap = mutableMapOf<String, Drawable>()
private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>()) private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope) val installedExtensionsFlow = installedExtensionMapFlow.mapExtensions(scope)
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
// SY --> // SY -->
val availableExtensionsFlow = _availableExtensionsMapFlow.map { it.filterNotBlacklisted().values.toList() } val availableExtensionsFlow = availableExtensionMapFlow.map { it.filterNotBlacklisted().values.toList() }
.stateIn(scope, SharingStarted.Lazily, _availableExtensionsMapFlow.value.values.toList()) .stateIn(scope, SharingStarted.Lazily, availableExtensionMapFlow.value.values.toList())
// SY <-- // SY <--
private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>()) private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope) val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensions(scope)
init { init {
initExtensions() initExtensions()
@@ -89,7 +90,7 @@ class ExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? { fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsMapFlow.value.values val pkgName = installedExtensionMapFlow.value.values
.find { ext -> .find { ext ->
ext.sources.any { it.id == sourceId } ext.sources.any { it.id == sourceId }
} }
@@ -128,11 +129,11 @@ class ExtensionManager(
private fun initExtensions() { private fun initExtensions() {
val extensions = ExtensionLoader.loadExtensions(context) val extensions = ExtensionLoader.loadExtensions(context)
_installedExtensionsMapFlow.value = extensions installedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>() .filterIsInstance<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension } .associate { it.extension.pkgName to it.extension }
_untrustedExtensionsMapFlow.value = extensions untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension } .associate { it.extension.pkgName to it.extension }
// SY --> // SY -->
@@ -159,7 +160,7 @@ class ExtensionManager(
// EXH <-- // EXH <--
/** /**
* Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow]. * Finds the available extensions in the [api] and updates [availableExtensionMapFlow].
*/ */
suspend fun findAvailableExtensions() { suspend fun findAvailableExtensions() {
val extensions: List<Extension.Available> = try { val extensions: List<Extension.Available> = try {
@@ -172,7 +173,7 @@ class ExtensionManager(
enableAdditionalSubLanguages(extensions) enableAdditionalSubLanguages(extensions)
_availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName } availableExtensionMapFlow.value = extensions.associateBy { it.pkgName }
updatedInstalledExtensionsStatuses(extensions) updatedInstalledExtensionsStatuses(extensions)
setupAvailableExtensionsSourcesDataMap(extensions) setupAvailableExtensionsSourcesDataMap(extensions)
} }
@@ -218,7 +219,7 @@ class ExtensionManager(
return return
} }
val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap() val installedExtensionsMap = installedExtensionMapFlow.value.toMutableMap()
var changed = false var changed = false
for ((pkgName, extension) in installedExtensionsMap) { for ((pkgName, extension) in installedExtensionsMap) {
val availableExt = availableExtensions.find { it.pkgName == pkgName } val availableExt = availableExtensions.find { it.pkgName == pkgName }
@@ -247,7 +248,7 @@ class ExtensionManager(
} }
} }
if (changed) { if (changed) {
_installedExtensionsMapFlow.value = installedExtensionsMap installedExtensionMapFlow.value = installedExtensionsMap
} }
updatePendingUpdatesCount() updatePendingUpdatesCount()
} }
@@ -271,7 +272,7 @@ class ExtensionManager(
* @param extension The extension to be updated. * @param extension The extension to be updated.
*/ */
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> { fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow() val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
return installExtension(availableExt) return installExtension(availableExt)
} }
@@ -308,11 +309,11 @@ class ExtensionManager(
* @param extension the extension to trust * @param extension the extension to trust
*/ */
suspend fun trust(extension: Extension.Untrusted) { suspend fun trust(extension: Extension.Untrusted) {
_untrustedExtensionsMapFlow.value[extension.pkgName] ?: return untrustedExtensionMapFlow.value[extension.pkgName] ?: return
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
_untrustedExtensionsMapFlow.value -= extension.pkgName untrustedExtensionMapFlow.value -= extension.pkgName
ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
.let { it as? LoadResult.Success } .let { it as? LoadResult.Success }
@@ -332,7 +333,7 @@ class ExtensionManager(
} }
// SY <-- // SY <--
_installedExtensionsMapFlow.value += extension installedExtensionMapFlow.value += extension
} }
/** /**
@@ -349,7 +350,7 @@ class ExtensionManager(
} }
// SY <-- // SY <--
_installedExtensionsMapFlow.value += extension installedExtensionMapFlow.value += extension
} }
/** /**
@@ -359,8 +360,8 @@ class ExtensionManager(
* @param pkgName The package name of the uninstalled application. * @param pkgName The package name of the uninstalled application.
*/ */
private fun unregisterExtension(pkgName: String) { private fun unregisterExtension(pkgName: String) {
_installedExtensionsMapFlow.value -= pkgName installedExtensionMapFlow.value -= pkgName
_untrustedExtensionsMapFlow.value -= pkgName untrustedExtensionMapFlow.value -= pkgName
} }
/** /**
@@ -379,8 +380,8 @@ class ExtensionManager(
} }
override fun onExtensionUntrusted(extension: Extension.Untrusted) { override fun onExtensionUntrusted(extension: Extension.Untrusted) {
_installedExtensionsMapFlow.value -= extension.pkgName installedExtensionMapFlow.value -= extension.pkgName
_untrustedExtensionsMapFlow.value += extension untrustedExtensionMapFlow.value += extension
updatePendingUpdatesCount() updatePendingUpdatesCount()
} }
@@ -404,14 +405,14 @@ class ExtensionManager(
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension val availableExt = availableExtension
?: _availableExtensionsMapFlow.value[pkgName] ?: availableExtensionMapFlow.value[pkgName]
?: return false ?: return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
} }
private fun updatePendingUpdatesCount() { private fun updatePendingUpdatesCount() {
val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate } val pendingUpdateCount = installedExtensionMapFlow.value.values.count { it.hasUpdate }
preferences.extensionUpdatesCount().set(pendingUpdateCount) preferences.extensionUpdatesCount().set(pendingUpdateCount)
if (pendingUpdateCount == 0) { if (pendingUpdateCount == 0) {
ExtensionUpdateNotifier(context).dismiss() ExtensionUpdateNotifier(context).dismiss()
@@ -34,8 +34,10 @@ internal class ExtensionApi {
private val getExtensionRepo: GetExtensionRepo by injectLazy() private val getExtensionRepo: GetExtensionRepo by injectLazy()
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
private val extensionManager: ExtensionManager by injectLazy() private val extensionManager: ExtensionManager by injectLazy()
// SY --> // SY -->
private val sourcePreferences: SourcePreferences by injectLazy() private val sourcePreferences: SourcePreferences by injectLazy()
// SY <-- // SY <--
private val json: Json by injectLazy() private val json: Json by injectLazy()
@@ -3,15 +3,20 @@ package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionUpdateNotifier(private val context: Context) { class ExtensionUpdateNotifier(
private val context: Context,
private val securityPreferences: SecurityPreferences = Injekt.get(),
) {
fun promptUpdates(names: List<String>) { fun promptUpdates(names: List<String>) {
context.notify( context.notify(
Notifications.ID_UPDATES_TO_EXTS, Notifications.ID_UPDATES_TO_EXTS,
@@ -24,9 +29,11 @@ class ExtensionUpdateNotifier(private val context: Context) {
names.size, names.size,
), ),
) )
val extNames = names.joinToString(", ") if (!securityPreferences.hideNotificationContent().get()) {
setContentText(extNames) val extNames = names.joinToString(", ")
setStyle(NotificationCompat.BigTextStyle().bigText(extNames)) setContentText(extNames)
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
}
setSmallIcon(R.drawable.ic_extension_24dp) setSmallIcon(R.drawable.ic_extension_24dp)
setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context)) setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context))
setAutoCancel(true) setAutoCancel(true)
@@ -1,7 +1,13 @@
package eu.kanade.tachiyomi.extension.model package eu.kanade.tachiyomi.extension.model
enum class InstallStep { enum class InstallStep {
Idle, Pending, Downloading, Installing, Installed, Error; Idle,
Pending,
Downloading,
Installing,
Installed,
Error,
;
fun isCompleted(): Boolean { fun isCompleted(): Boolean {
return this == Installed || this == Error || this == Idle return this == Installed || this == Error || this == Idle
@@ -223,7 +223,6 @@ internal object ExtensionLoader {
* @param context The application context. * @param context The application context.
* @param extensionInfo The extension to load. * @param extensionInfo The extension to load.
*/ */
@Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount")
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val pkgManager = context.packageManager val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo val pkgInfo = extensionInfo.packageInfo
@@ -383,7 +383,7 @@ class EHentai(
doc.select("#gdd .gdt1").find { el -> doc.select("#gdd .gdt1").find { el ->
el.text().lowercase() == "posted:" el.text().lowercase() == "posted:"
}!!.nextElementSibling()!!.text(), }!!.nextElementSibling()!!.text(),
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC) MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
)!!.toInstant().toEpochMilli(), )!!.toInstant().toEpochMilli(),
scanlator = EHentaiSearchMetadata.galleryId(location), scanlator = EHentaiSearchMetadata.galleryId(location),
) )
@@ -401,7 +401,7 @@ class EHentai(
chapter_number = index + 2f, chapter_number = index + 2f,
date_upload = ZonedDateTime.parse( date_upload = ZonedDateTime.parse(
posted, posted,
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC) MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
).toInstant().toEpochMilli(), ).toInstant().toEpochMilli(),
scanlator = EHentaiSearchMetadata.galleryId(link), scanlator = EHentaiSearchMetadata.galleryId(link),
) )
@@ -542,9 +542,10 @@ class EHentai(
if ( if (
MATCH_SEEK_REGEX.matches(jumpSeekValue) || MATCH_SEEK_REGEX.matches(jumpSeekValue) ||
( (
MATCH_YEAR_REGEX.matches(jumpSeekValue) && jumpSeekValue.toIntOrNull()?.let { MATCH_YEAR_REGEX.matches(jumpSeekValue) &&
it in 2007..2099 jumpSeekValue.toIntOrNull()?.let {
} == true it in 2007..2099
} == true
) )
) { ) {
uri.appendQueryParameter("seek", jumpSeekValue) uri.appendQueryParameter("seek", jumpSeekValue)
@@ -715,7 +716,7 @@ class EHentai(
when (left.removeSuffix(":").lowercase()) { when (left.removeSuffix(":").lowercase()) {
"posted" -> datePosted = ZonedDateTime.parse( "posted" -> datePosted = ZonedDateTime.parse(
right, right,
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC) MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC),
).toInstant().toEpochMilli() ).toInstant().toEpochMilli()
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/ // Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/ // Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
@@ -87,6 +87,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false) private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty() private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty() private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty()
private fun coverQuality() = sourcePreferences.getString(getCoverQualityPrefKey(mdLang.lang), "").orEmpty()
private fun tryUsingFirstVolumeCover() = sourcePreferences.getBoolean(getTryUsingFirstVolumeCoverKey(mdLang.lang), false)
private val mangadexService by lazy { private val mangadexService by lazy {
MangaDexService(client) MangaDexService(client)
@@ -189,11 +191,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return mangaHandler.fetchMangaDetailsObservable(manga, id) return mangaHandler.fetchMangaDetailsObservable(manga, id, coverQuality(), tryUsingFirstVolumeCover())
} }
override suspend fun getMangaDetails(manga: SManga): SManga { override suspend fun getMangaDetails(manga: SManga): SManga {
return mangaHandler.getMangaDetails(manga, id) return mangaHandler.getMangaDetails(manga, id, coverQuality(), tryUsingFirstVolumeCover())
} }
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
@@ -239,7 +241,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override fun newMetaInstance() = MangaDexSearchMetadata() override fun newMetaInstance() = MangaDexSearchMetadata()
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) { override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) {
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third) apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third, null, coverQuality())
} }
// LoginSource methods // LoginSource methods
@@ -334,5 +336,17 @@ class MangaDex(delegate: HttpSource, val context: Context) :
fun getBlockedUploaderPrefKey(dexLang: String): String { fun getBlockedUploaderPrefKey(dexLang: String): String {
return "${blockedUploaderPref}_$dexLang" return "${blockedUploaderPref}_$dexLang"
} }
private const val coverQualityPref = "thumbnailQuality"
fun getCoverQualityPrefKey(dexLang: String): String {
return "${coverQualityPref}_$dexLang"
}
private const val tryUsingFirstVolumeCover = "tryUsingFirstVolumeCover"
fun getTryUsingFirstVolumeCoverKey(dexLang: String): String {
return "${tryUsingFirstVolumeCover}_$dexLang"
}
} }
} }
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.copy import eu.kanade.tachiyomi.source.model.copy
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -19,6 +18,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
@@ -42,6 +42,7 @@ class MergedSource : HttpSource() {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val downloadManager: DownloadManager by injectLazy() private val downloadManager: DownloadManager by injectLazy()
private val downloadPreferences: DownloadPreferences by injectLazy() private val downloadPreferences: DownloadPreferences by injectLazy()
private val filterChaptersForDownload: FilterChaptersForDownload by injectLazy()
override val id: Long = MERGED_SOURCE_ID override val id: Long = MERGED_SOURCE_ID
@@ -119,12 +120,6 @@ class MergedSource : HttpSource() {
"Manga references are empty, chapters unavailable, merge is likely corrupted" "Manga references are empty, chapters unavailable, merge is likely corrupted"
} }
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(
getCategories.await(manga.id).map {
it.id
},
downloadPreferences,
)
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
var exception: Exception? = null var exception: Exception? = null
return supervisorScope { return supervisorScope {
@@ -141,11 +136,15 @@ class MergedSource : HttpSource() {
val chapterList = source.getChapterList(loadedManga.toSManga()) val chapterList = source.getChapterList(loadedManga.toSManga())
val results = val results =
syncChaptersWithSource.await(chapterList, loadedManga, source) syncChaptersWithSource.await(chapterList, loadedManga, source)
if (ifDownloadNewChapters && reference.downloadChapters) {
downloadManager.downloadChapters( if (reference.downloadChapters) {
loadedManga, val chaptersToDownload = filterChaptersForDownload.await(manga, results)
results, if (chaptersToDownload.isNotEmpty()) {
) downloadManager.downloadChapters(
loadedManga,
chaptersToDownload,
)
}
} }
results results
} else { } else {
@@ -73,16 +73,17 @@ interface SecureActivityDelegate {
} }
val lockedDays = preferences.authenticatorDays().get() val lockedDays = preferences.authenticatorDays().get()
val canLockToday = lockedDays == LOCK_ALL_DAYS || when (today.get(Calendar.DAY_OF_WEEK)) { val canLockToday = lockedDays == LOCK_ALL_DAYS ||
Calendar.SUNDAY -> (lockedDays and LOCK_SUNDAY) == LOCK_SUNDAY when (today.get(Calendar.DAY_OF_WEEK)) {
Calendar.MONDAY -> (lockedDays and LOCK_MONDAY) == LOCK_MONDAY Calendar.SUNDAY -> (lockedDays and LOCK_SUNDAY) == LOCK_SUNDAY
Calendar.TUESDAY -> (lockedDays and LOCK_TUESDAY) == LOCK_TUESDAY Calendar.MONDAY -> (lockedDays and LOCK_MONDAY) == LOCK_MONDAY
Calendar.WEDNESDAY -> (lockedDays and LOCK_WEDNESDAY) == LOCK_WEDNESDAY Calendar.TUESDAY -> (lockedDays and LOCK_TUESDAY) == LOCK_TUESDAY
Calendar.THURSDAY -> (lockedDays and LOCK_THURSDAY) == LOCK_THURSDAY Calendar.WEDNESDAY -> (lockedDays and LOCK_WEDNESDAY) == LOCK_WEDNESDAY
Calendar.FRIDAY -> (lockedDays and LOCK_FRIDAY) == LOCK_FRIDAY Calendar.THURSDAY -> (lockedDays and LOCK_THURSDAY) == LOCK_THURSDAY
Calendar.SATURDAY -> (lockedDays and LOCK_SATURDAY) == LOCK_SATURDAY Calendar.FRIDAY -> (lockedDays and LOCK_FRIDAY) == LOCK_FRIDAY
else -> false Calendar.SATURDAY -> (lockedDays and LOCK_SATURDAY) == LOCK_SATURDAY
} else -> false
}
return canLockNow && canLockToday return canLockNow && canLockToday
} }
@@ -99,11 +100,13 @@ interface SecureActivityDelegate {
// `requireUnlock` can be true on process start or if app was closed in locked state // `requireUnlock` can be true on process start or if app was closed in locked state
if (!AuthenticatorUtil.isAuthenticating && !requireUnlock) { if (!AuthenticatorUtil.isAuthenticating && !requireUnlock) {
requireUnlock = /* SY --> */ canLockNow(preferences) && /* SY <-- */ when (val lockDelay = preferences.lockAppAfter().get()) { requireUnlock =
-1 -> false // Never /* SY --> */ canLockNow(preferences) &&
0 -> true // Always /* SY <-- */ when (val lockDelay = preferences.lockAppAfter().get()) {
else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis() -1 -> false // Never
} 0 -> true // Always
else -> lastClosedPref.get() + lockDelay * 60_000 <= System.currentTimeMillis()
}
} }
lastClosedPref.delete() lastClosedPref.delete()
@@ -140,7 +143,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
val incognitoModeFlow = preferences.incognitoMode().changes() val incognitoModeFlow = preferences.incognitoMode().changes()
combine(secureScreenFlow, incognitoModeFlow) { secureScreen, incognitoMode -> combine(secureScreenFlow, incognitoModeFlow) { secureScreen, incognitoMode ->
secureScreen == SecurityPreferences.SecureScreenMode.ALWAYS || secureScreen == SecurityPreferences.SecureScreenMode.ALWAYS ||
secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode (secureScreen == SecurityPreferences.SecureScreenMode.INCOGNITO && incognitoMode)
} }
.onEach(activity.window::setSecureScreen) .onEach(activity.window::setSecureScreen)
.launchIn(activity.lifecycleScope) .launchIn(activity.lifecycleScope)
@@ -41,7 +41,7 @@ class ExtensionsScreenModel(
private val getExtensions: GetExtensionsByType = Injekt.get(), private val getExtensions: GetExtensionsByType = Injekt.get(),
) : StateScreenModel<ExtensionsScreenModel.State>(State()) { ) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
init { init {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@@ -62,14 +62,20 @@ class ExtensionsScreenModel(
it.name.contains(input, ignoreCase = true) || it.name.contains(input, ignoreCase = true) ||
it.baseUrl.contains(input, ignoreCase = true) || it.baseUrl.contains(input, ignoreCase = true) ||
it.id == input.toLongOrNull() it.id == input.toLongOrNull()
} || extension.name.contains(input, ignoreCase = true) } ||
extension.name.contains(input, ignoreCase = true)
} }
is Extension.Installed -> { is Extension.Installed -> {
extension.sources.any { extension.sources.any {
it.name.contains(input, ignoreCase = true) || it.name.contains(input, ignoreCase = true) ||
it.id == input.toLongOrNull() || it.id == input.toLongOrNull() ||
if (it is HttpSource) { it.baseUrl.contains(input, ignoreCase = true) } else false if (it is HttpSource) {
} || extension.name.contains(input, ignoreCase = true) it.baseUrl.contains(input, ignoreCase = true)
} else {
false
}
} ||
extension.name.contains(input, ignoreCase = true)
} }
is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true) is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true)
} }
@@ -80,7 +86,7 @@ class ExtensionsScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
combine( combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
_currentDownloads, currentDownloads,
getExtensions.subscribe(), getExtensions.subscribe(),
) { query, downloads, (_updates, _installed, _available, _untrusted) -> ) { query, downloads, (_updates, _installed, _available, _untrusted) ->
val searchQuery = query ?: "" val searchQuery = query ?: ""
@@ -103,7 +109,8 @@ class ExtensionsScreenModel(
.groupBy { it.lang } .groupBy { it.lang }
.toSortedMap(LocaleHelper.comparator) .toSortedMap(LocaleHelper.comparator)
.map { (lang, exts) -> .map { (lang, exts) ->
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to exts.map(extensionMapper(downloads)) ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
exts.map(extensionMapper(downloads))
} }
if (languagesWithExtensions.isNotEmpty()) { if (languagesWithExtensions.isNotEmpty()) {
itemsGroups.putAll(languagesWithExtensions) itemsGroups.putAll(languagesWithExtensions)
@@ -165,11 +172,11 @@ class ExtensionsScreenModel(
} }
private fun addDownloadState(extension: Extension, installStep: InstallStep) { private fun addDownloadState(extension: Extension, installStep: InstallStep) {
_currentDownloads.update { it + Pair(extension.pkgName, installStep) } currentDownloads.update { it + Pair(extension.pkgName, installStep) }
} }
private fun removeDownloadState(extension: Extension) { private fun removeDownloadState(extension: Extension) {
_currentDownloads.update { it - extension.pkgName } currentDownloads.update { it - extension.pkgName }
} }
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: Extension) = private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: Extension) =
@@ -121,7 +121,11 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
) )
val onDismissRequest = { screenModel.dialog.value = null } val onDismissRequest = { screenModel.dialog.value = null }
when (@Suppress("NAME_SHADOWING") val dialog = dialog) { when
(
@Suppress("NAME_SHADOWING")
val dialog = dialog
) {
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> { is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
MigrationMangaDialog( MigrationMangaDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -223,7 +223,9 @@ class MigrationListScreenModel(
smartSearchEngine.normalSearch(source, mangaObj.ogTitle) smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
} }
if (searchResult != null && !(searchResult.url == mangaObj.url && source.id == mangaObj.source)) { if (searchResult != null &&
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
) {
val localManga = networkToLocalManga.await(searchResult) val localManga = networkToLocalManga.await(searchResult)
val chapters = if (source is EHentai) { val chapters = if (source is EHentai) {
@@ -237,7 +239,8 @@ class MigrationListScreenModel(
} catch (e: Exception) { } catch (e: Exception) {
return@async2 null return@async2 null
} }
manga.progress.value = validSources.size to processedSources.incrementAndGet() manga.progress.value =
validSources.size to processedSources.incrementAndGet()
localManga to chapters.size localManga to chapters.size
} else { } else {
null null
@@ -314,7 +317,8 @@ class MigrationListScreenModel(
if (result == null && hideNotFound) { if (result == null && hideNotFound) {
removeManga(manga) removeManga(manga)
} }
if (result != null && showOnlyUpdates && if (result != null &&
showOnlyUpdates &&
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0) (getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
) { ) {
removeManga(manga) removeManga(manga)
@@ -363,7 +367,10 @@ class MigrationListScreenModel(
dbChapters.forEach { chapter -> dbChapters.forEach { chapter ->
if (chapter.isRecognizedNumber) { if (chapter.isRecognizedNumber) {
val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber } val prevChapter = prevMangaChapters.find {
it.isRecognizedNumber &&
it.chapterNumber == chapter.chapterNumber
}
if (prevChapter != null) { if (prevChapter != null) {
chapterUpdates += ChapterUpdate( chapterUpdates += ChapterUpdate(
id = chapter.id, id = chapter.id,
@@ -119,7 +119,10 @@ class SourcesScreenModel(
items = byLang items = byLang
.flatMap { .flatMap {
listOf( listOf(
SourceUiModel.Header(it.key.removePrefix(CATEGORY_KEY_PREFIX), it.value.firstOrNull()?.category != null), SourceUiModel.Header(
it.key.removePrefix(CATEGORY_KEY_PREFIX),
it.value.firstOrNull()?.category != null,
),
*it.value.map { source -> *it.value.map { source ->
SourceUiModel.Item(source) SourceUiModel.Item(source)
}.toTypedArray(), }.toTypedArray(),
@@ -48,7 +48,15 @@ class BiometricTimesScreen : Screen() {
fun showTimePicker(startTime: Duration? = null) { fun showTimePicker(startTime: Duration? = null) {
val activity = context as? MainActivity ?: return val activity = context as? MainActivity ?: return
val picker = MaterialTimePicker.Builder() val picker = MaterialTimePicker.Builder()
.setTitleText(if (startTime == null) SYMR.strings.biometric_lock_start_time.getString(context) else SYMR.strings.biometric_lock_end_time.getString(context)) .setTitleText(
if (startTime ==
null
) {
SYMR.strings.biometric_lock_start_time.getString(context)
} else {
SYMR.strings.biometric_lock_end_time.getString(context)
},
)
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK) .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
.build() .build()
picker.addOnPositiveButtonClickListener { picker.addOnPositiveButtonClickListener {
@@ -70,8 +70,8 @@ object HomeScreen : Screen() {
private val openTabEvent = Channel<Tab>() private val openTabEvent = Channel<Tab>()
private val showBottomNavEvent = Channel<Boolean>() private val showBottomNavEvent = Channel<Boolean>()
private const val TabFadeDuration = 200 private const val TAB_FADE_DURATION = 200
private const val TabNavigatorKey = "HomeTabs" private const val TAB_NAVIGATOR_KEY = "HomeTabs"
private val tabs = listOf( private val tabs = listOf(
LibraryTab, LibraryTab,
@@ -94,7 +94,7 @@ object HomeScreen : Screen() {
TabNavigator( TabNavigator(
tab = LibraryTab, tab = LibraryTab,
key = TabNavigatorKey, key = TAB_NAVIGATOR_KEY,
) { tabNavigator -> ) { tabNavigator ->
// Provide usable navigator to content screen // Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) { CompositionLocalProvider(LocalNavigator provides navigator) {
@@ -144,8 +144,11 @@ object HomeScreen : Screen() {
AnimatedContent( AnimatedContent(
targetState = tabNavigator.current, targetState = tabNavigator.current,
transitionSpec = { transitionSpec = {
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith materialFadeThroughIn(
materialFadeThroughOut(durationMillis = TabFadeDuration) initialScale = 1f,
durationMillis = TAB_FADE_DURATION,
) togetherWith
materialFadeThroughOut(durationMillis = TAB_FADE_DURATION)
}, },
label = "tabContent", label = "tabContent",
) { ) {
@@ -184,7 +184,11 @@ class LibraryScreenModel(
.applyGrouping(groupType) .applyGrouping(groupType)
// SY <-- // SY <--
.applyFilters(tracks, trackingFiler) .applyFilters(tracks, trackingFiler)
.applySort(tracks, trackingFiler.keys,/* SY --> */sort.takeIf { groupType != LibraryGroup.BY_DEFAULT } /* SY <-- */) .applySort(
tracks, trackingFiler.keys, /* SY --> */sort.takeIf {
groupType != LibraryGroup.BY_DEFAULT
}, /* SY <-- */
)
.mapValues { (_, value) -> .mapValues { (_, value) ->
if (searchQuery != null) { if (searchQuery != null) {
// Filter query // Filter query
@@ -278,7 +282,6 @@ class LibraryScreenModel(
/** /**
* Applies library filters to the given map of manga. * Applies library filters to the given map of manga.
*/ */
@Suppress("LongMethod", "CyclomaticComplexMethod")
private suspend fun LibraryMap.applyFilters( private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
trackingFiler: Map<Long, TriState>, trackingFiler: Map<Long, TriState>,
@@ -373,7 +376,6 @@ class LibraryScreenModel(
/** /**
* Applies library sorting to the given map of manga. * Applies library sorting to the given map of manga.
*/ */
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun LibraryMap.applySort( private fun LibraryMap.applySort(
// Map<MangaId, List<Track>> // Map<MangaId, List<Track>>
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
@@ -387,7 +389,8 @@ class LibraryScreenModel(
.asSequence() .asSequence()
.mapNotNull { .mapNotNull {
val list = it.split("|") val list = it.split("|")
(list.getOrNull(0)?.toIntOrNull() ?: return@mapNotNull null) to (list.getOrNull(1) ?: return@mapNotNull null) (list.getOrNull(0)?.toIntOrNull() ?: return@mapNotNull null) to
(list.getOrNull(1) ?: return@mapNotNull null)
} }
.sortedBy { it.first } .sortedBy { it.first }
.map { it.second } .map { it.second }
@@ -453,8 +456,12 @@ class LibraryScreenModel(
} }
// SY --> // SY -->
LibrarySort.Type.TagList -> { LibrarySort.Type.TagList -> {
val manga1IndexOfTag = listOfTags.indexOfFirst { i1.libraryManga.manga.genre?.contains(it) ?: false } val manga1IndexOfTag = listOfTags.indexOfFirst {
val manga2IndexOfTag = listOfTags.indexOfFirst { i2.libraryManga.manga.genre?.contains(it) ?: false } i1.libraryManga.manga.genre?.contains(it) ?: false
}
val manga2IndexOfTag = listOfTags.indexOfFirst {
i2.libraryManga.manga.genre?.contains(it) ?: false
}
manga1IndexOfTag.compareTo(manga2IndexOfTag) manga1IndexOfTag.compareTo(manga2IndexOfTag)
} }
// SY <-- // SY <--
@@ -822,9 +829,12 @@ class LibraryScreenModel(
if (source != null) { if (source != null) {
if (source is MergedSource) { if (source is MergedSource) {
val mergedMangas = getMergedMangaById.await(manga.id) val mergedMangas = getMergedMangaById.await(manga.id)
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } val sources = mergedMangas.distinctBy {
it.source
}.map { sourceManager.getOrStub(it.source) }
mergedMangas.forEach merge@{ mergedManga -> mergedMangas.forEach merge@{ mergedManga ->
val mergedSource = sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge val mergedSource =
sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge
downloadManager.deleteManga(mergedManga, mergedSource) downloadManager.deleteManga(mergedManga, mergedSource)
} }
} else { } else {
@@ -903,10 +913,11 @@ class LibraryScreenModel(
} else { } else {
categoryName categoryName
} }
LibraryGroup.BY_TRACK_STATUS -> TrackStatus.entries LibraryGroup.BY_TRACK_STATUS ->
.find { it.int.toLong() == category?.id } TrackStatus.entries
.let { it ?: TrackStatus.OTHER } .find { it.int.toLong() == category?.id }
.let { context.stringResource(it.res) } .let { it ?: TrackStatus.OTHER }
.let { context.stringResource(it.res) }
LibraryGroup.UNGROUPED -> context.stringResource(SYMR.strings.ungrouped) LibraryGroup.UNGROUPED -> context.stringResource(SYMR.strings.ungrouped)
else -> categoryName else -> categoryName
} }
@@ -993,49 +1004,66 @@ class LibraryScreenModel(
(manga.description?.contains(query, true) == true) || (manga.description?.contains(query, true) == true) ||
(source?.name?.contains(query, true) == true) || (source?.name?.contains(query, true) == true) ||
(sourceIdString != null && sourceIdString == query) || (sourceIdString != null && sourceIdString == query) ||
(loggedInTrackServices.isNotEmpty() && tracks != null && filterTracks(query, tracks, context)) || (
loggedInTrackServices.isNotEmpty() &&
tracks != null &&
filterTracks(query, tracks, context)
) ||
(genre.fastAny { it.contains(query, true) }) || (genre.fastAny { it.contains(query, true) }) ||
(searchTags?.fastAny { it.name.contains(query, true) } == true) || (searchTags?.fastAny { it.name.contains(query, true) } == true) ||
(searchTitles?.fastAny { it.title.contains(query, true) } == true) (searchTitles?.fastAny { it.title.contains(query, true) } == true)
} }
is Namespace -> { is Namespace -> {
searchTags != null && searchTags.fastAny { searchTags != null &&
val tag = queryComponent.tag searchTags.fastAny {
(it.namespace.equals(queryComponent.namespace, true) && tag?.run { it.name.contains(tag.asQuery(), true) } == true) || val tag = queryComponent.tag
(tag == null && it.namespace.equals(queryComponent.namespace, true)) (
} it.namespace.equals(queryComponent.namespace, true) &&
tag?.run { it.name.contains(tag.asQuery(), true) } == true
) ||
(tag == null && it.namespace.equals(queryComponent.namespace, true))
}
} }
else -> true else -> true
} }
true -> when (queryComponent) { true -> when (queryComponent) {
is Text -> { is Text -> {
val query = queryComponent.asQuery() val query = queryComponent.asQuery()
query.isBlank() || ( query.isBlank() ||
(!manga.title.contains(query, true)) && (
(manga.author?.contains(query, true) != true) && (!manga.title.contains(query, true)) &&
(manga.artist?.contains(query, true) != true) && (manga.author?.contains(query, true) != true) &&
(manga.description?.contains(query, true) != true) && (manga.artist?.contains(query, true) != true) &&
(source?.name?.contains(query, true) != true) && (manga.description?.contains(query, true) != true) &&
(sourceIdString != null && sourceIdString != query) && (source?.name?.contains(query, true) != true) &&
(loggedInTrackServices.isEmpty() || tracks == null || !filterTracks(query, tracks, context)) && (sourceIdString != null && sourceIdString != query) &&
(!genre.fastAny { it.contains(query, true) }) && (
(searchTags?.fastAny { it.name.contains(query, true) } != true) && loggedInTrackServices.isEmpty() ||
(searchTitles?.fastAny { it.title.contains(query, true) } != true) tracks == null ||
) !filterTracks(query, tracks, context)
) &&
(!genre.fastAny { it.contains(query, true) }) &&
(searchTags?.fastAny { it.name.contains(query, true) } != true) &&
(searchTitles?.fastAny { it.title.contains(query, true) } != true)
)
} }
is Namespace -> { is Namespace -> {
val searchedTag = queryComponent.tag?.asQuery() val searchedTag = queryComponent.tag?.asQuery()
searchTags == null || (queryComponent.namespace.isBlank() && searchedTag.isNullOrBlank()) || searchTags.fastAll { mangaTag -> searchTags == null ||
if (queryComponent.namespace.isBlank() && !searchedTag.isNullOrBlank()) { (queryComponent.namespace.isBlank() && searchedTag.isNullOrBlank()) ||
!mangaTag.name.contains(searchedTag, true) searchTags.fastAll { mangaTag ->
} else if (searchedTag.isNullOrBlank()) { if (queryComponent.namespace.isBlank() && !searchedTag.isNullOrBlank()) {
mangaTag.namespace == null || !mangaTag.namespace.equals(queryComponent.namespace, true) !mangaTag.name.contains(searchedTag, true)
} else if (mangaTag.namespace.isNullOrBlank()) { } else if (searchedTag.isNullOrBlank()) {
true mangaTag.namespace == null ||
} else { !mangaTag.namespace.equals(queryComponent.namespace, true)
!mangaTag.name.contains(searchedTag, true) || !mangaTag.namespace.equals(queryComponent.namespace, true) } else if (mangaTag.namespace.isNullOrBlank()) {
true
} else {
!mangaTag.name.contains(searchedTag, true) ||
!mangaTag.namespace.equals(queryComponent.namespace, true)
}
} }
}
} }
else -> true else -> true
} }
@@ -34,7 +34,7 @@ class LibrarySettingsScreenModel(
.stateIn( .stateIn(
scope = screenModelScope, scope = screenModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = trackerManager.loggedInTrackers() initialValue = trackerManager.loggedInTrackers(),
) )
// SY --> // SY -->

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