mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-17 22:22:26 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bde967291 | ||
|
|
b40f1d3975 | ||
|
|
42daa3f432 | ||
|
|
f8e82b9322 | ||
|
|
cb0e329716 | ||
|
|
b58d2a5d4b | ||
|
|
d3dcb7fe4b | ||
|
|
56b70ef529 | ||
|
|
5cf305b620 | ||
|
|
87665736a9 | ||
|
|
2f9edb551f | ||
|
|
d4b88b13b8 | ||
|
|
99edfbe4a8 | ||
|
|
fa7155896c | ||
|
|
51fb9c0b76 | ||
|
|
c495753315 | ||
|
|
11ced33fdb | ||
|
|
8c480c6355 | ||
|
|
e9ca71d837 | ||
|
|
e4091c0160 | ||
|
|
a2d22a7d55 | ||
|
|
db22517339 | ||
|
|
33faa5a356 | ||
|
|
8c5ae44f51 | ||
|
|
575d7e8b89 | ||
|
|
8c00b44eda | ||
|
|
77a86d486f | ||
|
|
4b379576f4 | ||
|
|
be88ce199c | ||
|
|
32b8ef6240 | ||
|
|
f82b8300b0 | ||
|
|
ce5820b496 | ||
|
|
f6b2684323 | ||
|
|
0d768af3d0 | ||
|
|
08f0bcc0d8 | ||
|
|
a3c45a05b8 | ||
|
|
6926914385 | ||
|
|
f3d5491a7d | ||
|
|
81871a3469 | ||
|
|
0024278f4b | ||
|
|
f9f938f5af | ||
|
|
1d7c838ae6 | ||
|
|
d219c5e3bb | ||
|
|
cc3f640faa | ||
|
|
84265febf3 | ||
|
|
171a06a8ba | ||
|
|
565379779f | ||
|
|
0394714274 | ||
|
|
75b445fa8f | ||
|
|
0cc724108b | ||
|
|
1e6c35d887 | ||
|
|
5b85084d0a | ||
|
|
8b2d35f306 | ||
|
|
47816d4b21 | ||
|
|
89bbdb17fb | ||
|
|
3c6f0f1697 | ||
|
|
ab214526c6 | ||
|
|
e6cc38de06 | ||
|
+12 |
7c7d35e2c6 | ||
|
|
9867c160f7 | ||
|
|
20c899f2cd | ||
|
|
3f64529412 | ||
|
|
27749fc583 | ||
|
|
2248bdac54 | ||
|
|
42c7329a9e | ||
|
|
22d5c9d9f4 | ||
|
|
5be5a4e819 | ||
|
|
b924d582f4 | ||
|
|
6f601a18c5 | ||
|
|
1ff9a5625c | ||
|
|
cae9fbf321 | ||
|
|
d17976c910 | ||
|
|
b609166702 | ||
|
|
cf93afab45 | ||
|
|
b543bc089a | ||
|
|
4e4bdffdf3 | ||
|
|
100cea0757 | ||
|
|
86344e20d3 | ||
|
|
e092b4208a | ||
|
|
1bf4eff931 | ||
|
|
b70edfac58 | ||
|
|
9c0eebb55c | ||
|
|
f6f1d13add | ||
|
|
31a9ff261e | ||
|
|
c0a1203541 | ||
|
|
82ffc8efa6 | ||
|
|
d6e17b04c3 | ||
|
|
e6c6700351 | ||
|
|
ede2f56bc6 | ||
|
|
2037ec4500 | ||
|
|
533a578bdb | ||
|
|
fa966fcb97 | ||
|
|
05d90ea4d6 | ||
|
|
49c4d08b22 | ||
|
|
bbe9aa8561 | ||
|
|
13975d6f7e | ||
|
|
e659e90c26 | ||
|
|
99e6fa3c06 | ||
|
|
cf13012629 | ||
|
|
93b355a9ef | ||
|
|
a4f5a8184c | ||
|
|
4ce249c1a0 | ||
|
|
edcf84d902 | ||
|
|
bd5c4d48f9 | ||
|
|
51b3ab3fd1 | ||
|
|
e6f72000ba | ||
|
|
9bf2d78a42 | ||
|
|
89c4e3bb39 | ||
|
|
47fe792ddc | ||
|
|
906d6f3cdb | ||
|
|
e059190fab | ||
|
|
a39b5a56e8 | ||
|
|
3e6afee13b | ||
|
|
9a11ec8ead | ||
|
|
1263df9d41 | ||
|
|
c96b6ae562 | ||
|
|
23c427cf60 | ||
|
|
e3260d56f7 | ||
|
|
f37afbcec9 | ||
|
|
7a1c8a1b61 | ||
|
|
532b5cf290 | ||
|
|
3cb1b2e17a | ||
|
|
4c9cfd8da5 | ||
|
|
2ecb70a590 | ||
|
|
2e0786f699 | ||
|
|
e7e4d9b6b3 | ||
|
|
5fe7dd9f06 | ||
|
|
4cb05cc738 | ||
|
|
876c3f951b | ||
|
|
08a61a42e9 | ||
|
|
cadd36ad9a | ||
|
|
97327555b3 | ||
|
|
5b88f88198 | ||
|
|
556371e1c8 | ||
|
|
c222a28bd1 | ||
|
|
7161bc2e82 | ||
|
|
c3d858a561 | ||
|
|
13552c5ffa | ||
|
|
e6ca458e1f | ||
|
|
05c7df2ed3 | ||
|
|
4a3339a21f | ||
|
|
6f497ed031 | ||
|
|
8b862c2e46 | ||
|
|
fc2c8c06a9 | ||
|
|
c17fc6792a | ||
|
|
9e27ae3d2b | ||
|
|
8b5dd39f1c | ||
|
|
8d81c94679 | ||
|
|
e5a693f224 | ||
|
|
7947e1bd6a | ||
|
|
cab729c939 | ||
|
|
f6a95a7416 | ||
|
|
32db0a5f4c | ||
|
|
2ec67ac0c1 | ||
|
|
c2bc95191b | ||
|
|
b1b79a63f0 | ||
|
|
ec99ab3aef | ||
|
|
69e5a1aea7 | ||
|
+2 |
84d620978b | ||
|
|
412815af06 | ||
|
|
f7fb68692a | ||
|
|
aa300cb53e | ||
|
|
855eea2ada | ||
|
|
f4703ed83a | ||
|
|
506d51a007 | ||
|
|
9f9155121c | ||
|
|
282110ef21 | ||
|
|
ace387f8bf | ||
|
|
5e428071c9 | ||
|
|
0acd80dd95 | ||
|
|
bdb0ce4779 | ||
|
|
e8bdf58530 | ||
|
|
e36b4ce60b | ||
|
|
6d543024a3 | ||
|
|
0e0b6d9283 | ||
|
|
38b1bd7383 | ||
|
+20 |
8609553896 | ||
|
|
5f0c460668 | ||
|
|
ac28b6c80c | ||
|
|
cc28776735 | ||
|
|
6ab87c7931 | ||
|
|
8662f80fbf | ||
|
|
6508766ccd | ||
|
|
09ec9fc8c5 | ||
|
|
87c6f34a55 | ||
|
|
643762f913 | ||
|
|
7e880014b0 | ||
|
|
f36c259c1f | ||
|
|
aef3beb15f | ||
|
|
e9469451ac | ||
|
|
f9793d3323 | ||
|
|
93ba6acea5 | ||
|
|
5e7fecc2c1 | ||
|
|
343074da5f | ||
|
|
7c08b75555 | ||
|
|
cbf72f4c60 | ||
|
|
0b6de39f2f | ||
|
|
72c4d1fdee | ||
|
|
fa96366b55 | ||
|
|
3ff25bc984 | ||
|
|
e9224bc2ba | ||
|
|
3c731c2cf5 | ||
|
|
5ac58d01b8 | ||
|
|
eefaf028ce | ||
|
|
582ccca1ab | ||
|
|
8f972115a8 | ||
|
|
6f6c033811 | ||
|
|
57a0ab6711 | ||
|
|
58b25d697f | ||
|
|
1a31c7c7ee | ||
|
|
ad6b651b37 | ||
|
|
96e5131358 | ||
|
|
1d5bc8d2c2 | ||
|
|
6cee911239 | ||
|
|
96347e3f76 | ||
|
|
9a45d248b1 | ||
|
|
04168ecec8 | ||
|
|
607f0ea9cd | ||
|
|
27a4f6f45c | ||
|
|
5236d003d2 | ||
|
|
d61a41e819 | ||
|
|
5637860dd2 | ||
|
|
d4d18d0898 | ||
|
|
065147472e | ||
|
|
6f635782c2 | ||
|
|
86d85f74c0 | ||
|
|
29e6a2c4a6 | ||
|
|
60c66bbd3a | ||
|
|
060e5b2e2e | ||
|
|
4ac9fcd4d3 | ||
|
|
d3b7f7e55f | ||
|
|
0d926626a1 | ||
|
|
6495a2ea43 | ||
|
|
94f711ba2a | ||
|
|
9f5c4e03b2 | ||
|
|
49562e1915 | ||
|
|
57c82b30ba | ||
|
|
e573f72cfd | ||
|
|
95357a8625 | ||
|
|
bd90307df9 | ||
|
|
16b5317b90 | ||
|
|
83f4b48629 | ||
|
|
4665dc50f6 | ||
|
|
85f5e5019e | ||
|
|
4bc3b9f3b6 | ||
|
|
2c0d3678d9 | ||
|
|
feda410152 | ||
|
|
200c2df5ba | ||
|
|
be09cddde2 | ||
|
|
498317de52 | ||
|
|
33b876edc6 | ||
|
|
e7251f2034 | ||
|
|
3d3c36078a | ||
|
|
c6a96b3970 | ||
|
|
fb3dc1c984 |
+2
-1
@@ -7,7 +7,7 @@ indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{xml,sq,sqm}]
|
||||
[*.{xml,sq,sqm,aidl}]
|
||||
indent_size = 4
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
@@ -29,3 +29,4 @@ ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_type-argument-comment = disabled
|
||||
ktlint_standard_type-parameter-comment = disabled
|
||||
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
21
|
||||
@@ -0,0 +1 @@
|
||||
patreon: mihon
|
||||
@@ -30,7 +30,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.19.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.19.6](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.19.1"
|
||||
Example: "0.19.6"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -95,7 +95,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.19.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.19.6](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
|
||||
@@ -26,20 +26,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
@@ -48,16 +48,24 @@ jobs:
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
|
||||
- name: Run unit tests
|
||||
id: unit_tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload test report
|
||||
if: steps.unit_tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: test-report-${{ github.sha }}
|
||||
path: app/build/reports/tests/testReleaseUnitTest
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release
|
||||
|
||||
@@ -29,16 +29,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mihon
|
||||
path: |
|
||||
@@ -83,16 +83,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mihon-foss
|
||||
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
@@ -143,12 +143,12 @@ jobs:
|
||||
mihon-foss
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||
body: |
|
||||
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving!
|
||||
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you're upgrading from an earlier version. Consider [becoming a patron](https://www.patreon.com/mihon/membership) to help keep Mihon improving!
|
||||
|
||||
<!-->
|
||||
|
||||
|
||||
+113
-3
@@ -12,6 +12,111 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.19.6] - 2026-03-23
|
||||
### Fixed
|
||||
- Fix app crashing when trying to add extension repo with existing signature ([@AntsyLich](https://github.com/AntsyLich)) ([`cb0e329`](https://github.com/mihonapp/mihon/commit/cb0e329))
|
||||
- Potentially fix 'database is locked' crash ([@AntsyLich](https://github.com/AntsyLich)) ([`f8e82b9`](https://github.com/mihonapp/mihon/commit/f8e82b9))
|
||||
- Fix occasional crash when mass installing/uninstalling extension using `PackageManager` ([@AntsyLich](https://github.com/AntsyLich)) ([`42daa3f`](https://github.com/mihonapp/mihon/commit/42daa3f))
|
||||
- Fix app crash on startup on some Android TV ([@AntsyLich](https://github.com/AntsyLich)) ([`b40f1d3`](https://github.com/mihonapp/mihon/commit/b40f1d3))
|
||||
|
||||
## [v0.19.5] - 2026-03-20
|
||||
### Changed
|
||||
- Retry in reader now redownloads image ([@AntsyLich](https://github.com/AntsyLich)) ([#3089](https://github.com/mihonapp/mihon/pull/3089))
|
||||
|
||||
### Fixed
|
||||
- Fix performance regression introduced in v0.19.4 ([@AntsyLich](https://github.com/AntsyLich)) ([#3082](https://github.com/mihonapp/mihon/pull/3082))
|
||||
- Fix duplicate key crash in duplicate detection ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3040](https://github.com/mihonapp/mihon/pull/3040))
|
||||
- Fix MangaUpdates HTTP 4XX errors ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3021](https://github.com/mihonapp/mihon/pull/3021))
|
||||
- Fix WebView JavaScript dialogs popup after screen is closed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3041](https://github.com/mihonapp/mihon/pull/3041))
|
||||
- Fix extension actions disappearing after installing and uninstalling in same session ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3049](https://github.com/mihonapp/mihon/pull/3049))
|
||||
|
||||
## [v0.19.4] - 2026-02-25
|
||||
### Added
|
||||
- Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673))
|
||||
- Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833))
|
||||
- Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851))
|
||||
- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891))
|
||||
- Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927))
|
||||
- Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928))
|
||||
|
||||
### Improved
|
||||
- Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266))
|
||||
- Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832))
|
||||
- Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874))
|
||||
- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977))
|
||||
- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2978](https://github.com/mihonapp/mihon/pull/2978))
|
||||
|
||||
### Changed
|
||||
- Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||
- Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675))
|
||||
- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995))
|
||||
- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906))
|
||||
- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||
|
||||
### Fixed
|
||||
- Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680))
|
||||
- Fix shizuku installer not updating installed extensions ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2697](https://github.com/mihonapp/mihon/pull/2697))
|
||||
- Fix mass migration not using the same search queries as individual migration ([@AntsyLich](https://github.com/AntsyLich)) ([#2736](https://github.com/mihonapp/mihon/pull/2736))
|
||||
- Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784))
|
||||
- Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779))
|
||||
- Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837))
|
||||
- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816))
|
||||
- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2993](https://github.com/mihonapp/mihon/pull/2993))
|
||||
- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2994](https://github.com/mihonapp/mihon/pull/2994))
|
||||
- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000))
|
||||
- Fix crash when downloading multiple chapters with Arabic locale ([@MajorTanya](https://github.com/MajorTanya)) ([#2676](https://github.com/mihonapp/mihon/pull/2676))
|
||||
|
||||
### Other
|
||||
- Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836))
|
||||
|
||||
## [v0.19.3] - 2025-11-07
|
||||
### Improved
|
||||
- Improved various aspects of the WebView multi window support ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
|
||||
|
||||
### Removed
|
||||
- Revert "Fix reader tap zones triggering after scrolling was stopped by the user" due to introduction of regression ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||
|
||||
### Fixed
|
||||
- Fix WebView crash introduced in 0.19.2 ([@bapeey](https://github.com/bapeey)) ([#2649](https://github.com/mihonapp/mihon/pull/2649))
|
||||
- Fix extra padding appearing in reader after user interactions ([@AntsyLich](https://github.com/AntsyLich)) ([#2669](https://github.com/mihonapp/mihon/pull/2669))
|
||||
- Fix long strip reader not scrolling on consecutive taps ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||
|
||||
## [v0.19.2] - 2025-11-02
|
||||
### Added
|
||||
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||
- Option to customize the number of concurrent source and page downloads ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
|
||||
|
||||
### Changed
|
||||
- Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
|
||||
|
||||
### Improved
|
||||
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
|
||||
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||
- Auto refresh extension list whenever a repository is added or removed ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
|
||||
- Added proper multi window support in WebView instead of treating everything as a redirect ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2584](https://github.com/mihonapp/mihon/pull/2584))
|
||||
|
||||
### Fixed
|
||||
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
|
||||
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
|
||||
- Fix disabling incognito mode from notification ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2512](https://github.com/mihonapp/mihon/pull/2512))
|
||||
- Fix mass migration advanced search query building ([@AntsyLich](https://github.com/AntsyLich)) ([#2629](https://github.com/mihonapp/mihon/pull/2629))
|
||||
- Fix migration dialog migrating to wrong entry ([@AntsyLich](https://github.com/AntsyLich)) ([#2631](https://github.com/mihonapp/mihon/pull/2631))
|
||||
- Fix migration "Attempt to invoke virtual method" crash ([@AntsyLich](https://github.com/AntsyLich)) ([#2632](https://github.com/mihonapp/mihon/pull/2632))
|
||||
- Fix reader "Unable to edit key" error ([@AntsyLich](https://github.com/AntsyLich)) ([#2634](https://github.com/mihonapp/mihon/pull/2634))
|
||||
- Fix extension download stuck in pending state in some cases ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
|
||||
- Fix scrollbar not showing when animator duration scale animation is turned off ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2398](https://github.com/mihonapp/mihon/pull/2398))
|
||||
- Fix date picker not allowing the same start and finish date in negative time zones ([@AntsyLich](https://github.com/AntsyLich), [@kashish-aggarwal21](https://github.com/kashish-aggarwal21)) ([#2617](https://github.com/mihonapp/mihon/pull/2617))
|
||||
- Fix reader tap zones triggering after scrolling was stopped by the user ([@Naputt1](https://github.com/Naputt1), [@AntsyLich](https://github.com/AntsyLich)) ([#2518](https://github.com/mihonapp/mihon/pull/2518))
|
||||
- Fix reader page indicator being partially visible on some devices ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||
- Fix inconsistent system bar and reader app bar background ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||
- Fix transparent system bar background in reader on Android 15+ ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
|
||||
|
||||
### Other
|
||||
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
|
||||
- Fix Kitsu tracker to conform to tracker data structure properly ([@cpiber](https://github.com/cpiber)) ([#2609](https://github.com/mihonapp/mihon/pull/2609))
|
||||
- Update Suwayomi tracker to use GraphQL API instead of REST API ([@cpiber](https://github.com/cpiber)) ([#2585](https://github.com/mihonapp/mihon/pull/2585))
|
||||
|
||||
## [v0.19.1] - 2025-08-07
|
||||
### Changed
|
||||
- LocalSource now reads ComicInfo.xml file for chapter (if available) to display chapter title, number and scanlator ([@raxod502](https://github.com/radian-software)) ([#2332](https://github.com/mihonapp/mihon/pull/2332))
|
||||
@@ -19,7 +124,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
### Removed
|
||||
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
|
||||
|
||||
### Fixes
|
||||
### Fixed
|
||||
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
|
||||
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
|
||||
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
|
||||
@@ -65,7 +170,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
|
||||
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
|
||||
|
||||
### Fixes
|
||||
### Fixed
|
||||
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
|
||||
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
|
||||
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))
|
||||
@@ -397,7 +502,12 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
||||
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
||||
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.1...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.6...main
|
||||
[v0.19.6]: https://github.com/mihonapp/mihon/compare/v0.19.5...v0.19.6
|
||||
[v0.19.5]: https://github.com/mihonapp/mihon/compare/v0.19.4...v0.19.5
|
||||
[v0.19.4]: https://github.com/mihonapp/mihon/compare/v0.19.3...v0.19.4
|
||||
[v0.19.3]: https://github.com/mihonapp/mihon/compare/v0.19.2...v0.19.3
|
||||
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
|
||||
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
|
||||
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
||||
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0
|
||||
|
||||
@@ -12,7 +12,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://mihon.app/download)
|
||||
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://hosted.weblate.org/engage/mihon/)
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 13
|
||||
versionName = "0.19.1"
|
||||
versionCode = 19
|
||||
versionName = "0.19.5"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -138,9 +138,9 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
aidl = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
renderScript = false
|
||||
shaders = false
|
||||
}
|
||||
@@ -159,12 +159,14 @@ kotlin {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xannotation-default-target=param-property",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -198,7 +200,7 @@ dependencies {
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
|
||||
implementation(libs.bundles.sqlite)
|
||||
implementation(androidx.sqlite.bundled)
|
||||
|
||||
implementation(kotlinx.reflect)
|
||||
implementation(kotlinx.immutables)
|
||||
@@ -261,7 +263,6 @@ dependencies {
|
||||
implementation(libs.directionalviewpager) {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.richeditor.compose)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.bundles.voyager)
|
||||
@@ -271,6 +272,7 @@ dependencies {
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.bundles.markdown)
|
||||
implementation(libs.materialKolor)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package mihon.app.shizuku;
|
||||
|
||||
interface IShellInterface {
|
||||
void install(in AssetFileDescriptor apk) = 1;
|
||||
|
||||
void destroy() = 16777114;
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -157,6 +158,7 @@ class DomainModule : InjektModule {
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetBookmarkedChaptersByMangaId(get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
|
||||
@@ -9,19 +9,22 @@ import tachiyomi.i18n.MR
|
||||
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
private val preferenceStore: PreferenceStore,
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun downloadedOnly() = preferenceStore.getBoolean(
|
||||
val downloadedOnly: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
Preference.appStateKey("pref_downloaded_only"),
|
||||
false,
|
||||
)
|
||||
|
||||
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
||||
val incognitoMode: Preference<Boolean> = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
||||
|
||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||
val extensionInstaller: ExtensionInstallerPreference = ExtensionInstallerPreference(context, preferenceStore)
|
||||
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
val shownOnboardingFlow: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
Preference.appStateKey("onboarding_complete"),
|
||||
false,
|
||||
)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||
@@ -30,9 +33,17 @@ class BasePreferences(
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
|
||||
|
||||
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
||||
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
|
||||
"pref_hardware_bitmap_threshold",
|
||||
GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
)
|
||||
|
||||
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
|
||||
val alwaysDecodeLongStripWithSSIV: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"pref_always_decode_long_strip_with_ssiv",
|
||||
false,
|
||||
)
|
||||
|
||||
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class SetReadStatus(
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
}
|
||||
|
||||
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
||||
if (read && downloadPreferences.removeAfterMarkedAsRead.get()) {
|
||||
chaptersToUpdate
|
||||
.groupBy { it.mangaId }
|
||||
.forEach { (mangaId, chapters) ->
|
||||
|
||||
@@ -114,6 +114,7 @@ class SyncChaptersWithSource(
|
||||
downloadManager.isChapterDownloaded(
|
||||
dbChapter.name,
|
||||
dbChapter.scanlator,
|
||||
dbChapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
@@ -121,12 +122,14 @@ class SyncChaptersWithSource(
|
||||
if (shouldRenameChapter) {
|
||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||
}
|
||||
|
||||
var toChangeChapter = dbChapter.copy(
|
||||
name = chapter.name,
|
||||
chapterNumber = chapter.chapterNumber,
|
||||
scanlator = chapter.scanlator,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
)
|
||||
|
||||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
@@ -168,7 +171,7 @@ class SyncChaptersWithSource(
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
|
||||
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead.get()
|
||||
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
|
||||
@@ -26,6 +26,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ class GetExtensionLanguages(
|
||||
) {
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.enabledLanguages.changes(),
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguage, availableExtensions ->
|
||||
availableExtensions
|
||||
|
||||
@@ -15,7 +15,7 @@ class GetExtensionSources(
|
||||
val isMultiLangSingleSource =
|
||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
return preferences.disabledSources().changes().map { disabledSources ->
|
||||
return preferences.disabledSources.changes().map { disabledSources ->
|
||||
fun Source.isEnabled() = id.toString() !in disabledSources
|
||||
|
||||
extension.sources
|
||||
|
||||
@@ -13,10 +13,10 @@ class GetExtensionsByType(
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Extensions> {
|
||||
val showNsfwSources = preferences.showNsfwSource().get()
|
||||
val showNsfwSources = preferences.showNsfwSource.get()
|
||||
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.enabledLanguages.changes(),
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
|
||||
@@ -14,11 +14,11 @@ class TrustExtension(
|
||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions.get()
|
||||
}
|
||||
|
||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||
preferences.trustedExtensions().getAndSet { exts ->
|
||||
preferences.trustedExtensions.getAndSet { exts ->
|
||||
// Remove previously trusted versions
|
||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||
|
||||
@@ -27,6 +27,6 @@ class TrustExtension(
|
||||
}
|
||||
|
||||
fun revokeAll() {
|
||||
preferences.trustedExtensions().delete()
|
||||
preferences.trustedExtensions.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class UpdateManga(
|
||||
|
||||
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
|
||||
val title =
|
||||
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
|
||||
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles.get())) {
|
||||
remoteTitle
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
|
||||
|
||||
val Manga.downloadedFilter: TriState
|
||||
get() {
|
||||
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
|
||||
if (Injekt.get<BasePreferences>().downloadedOnly.get()) return TriState.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||
|
||||
@@ -17,10 +17,10 @@ class GetEnabledSources(
|
||||
|
||||
fun subscribe(): Flow<List<Source>> {
|
||||
return combine(
|
||||
preferences.pinnedSources().changes(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
preferences.lastUsedSource().changes(),
|
||||
preferences.pinnedSources.changes(),
|
||||
preferences.enabledLanguages.changes(),
|
||||
preferences.disabledSources.changes(),
|
||||
preferences.lastUsedSource.changes(),
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||
sources
|
||||
|
||||
@@ -13,19 +13,19 @@ class GetIncognitoState(
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
fun await(sourceId: Long?): Boolean {
|
||||
if (basePreferences.incognitoMode().get()) return true
|
||||
if (basePreferences.incognitoMode.get()) return true
|
||||
if (sourceId == null) return false
|
||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
||||
|
||||
return extensionPackage in sourcePreferences.incognitoExtensions().get()
|
||||
return extensionPackage in sourcePreferences.incognitoExtensions.get()
|
||||
}
|
||||
|
||||
fun subscribe(sourceId: Long?): Flow<Boolean> {
|
||||
if (sourceId == null) return basePreferences.incognitoMode().changes()
|
||||
if (sourceId == null) return basePreferences.incognitoMode.changes()
|
||||
|
||||
return combine(
|
||||
basePreferences.incognitoMode().changes(),
|
||||
sourcePreferences.incognitoExtensions().changes(),
|
||||
basePreferences.incognitoMode.changes(),
|
||||
sourcePreferences.incognitoExtensions.changes(),
|
||||
extensionManager.getExtensionPackageAsFlow(sourceId),
|
||||
) { incognito, incognitoExtensions, extensionPackage ->
|
||||
incognito || (extensionPackage in incognitoExtensions)
|
||||
|
||||
@@ -15,8 +15,8 @@ class GetLanguagesWithSources(
|
||||
|
||||
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
preferences.enabledLanguages.changes(),
|
||||
preferences.disabledSources.changes(),
|
||||
repository.getOnlineSources(),
|
||||
) { enabledLanguage, disabledSource, onlineSources ->
|
||||
val sortedSources = onlineSources.sortedWith(
|
||||
|
||||
@@ -16,8 +16,8 @@ class GetSourcesWithFavoriteCount(
|
||||
|
||||
fun subscribe(): Flow<List<Pair<Source, Long>>> {
|
||||
return combine(
|
||||
preferences.migrationSortingDirection().changes(),
|
||||
preferences.migrationSortingMode().changes(),
|
||||
preferences.migrationSortingDirection.changes(),
|
||||
preferences.migrationSortingMode.changes(),
|
||||
repository.getSourcesWithFavoriteCount(),
|
||||
) { direction, mode, list ->
|
||||
list
|
||||
|
||||
@@ -7,8 +7,8 @@ class SetMigrateSorting(
|
||||
) {
|
||||
|
||||
fun await(mode: Mode, direction: Direction) {
|
||||
preferences.migrationSortingMode().set(mode)
|
||||
preferences.migrationSortingDirection().set(direction)
|
||||
preferences.migrationSortingMode.set(mode)
|
||||
preferences.migrationSortingDirection.set(direction)
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
|
||||
@@ -7,7 +7,7 @@ class ToggleIncognito(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
fun await(extensions: String, enable: Boolean) {
|
||||
preferences.incognitoExtensions().getAndSet {
|
||||
preferences.incognitoExtensions.getAndSet {
|
||||
if (enable) it.plus(extensions) else it.minus(extensions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ class ToggleLanguage(
|
||||
) {
|
||||
|
||||
fun await(language: String) {
|
||||
val isEnabled = language in preferences.enabledLanguages().get()
|
||||
preferences.enabledLanguages().getAndSet { enabled ->
|
||||
val isEnabled = language in preferences.enabledLanguages.get()
|
||||
preferences.enabledLanguages.getAndSet { enabled ->
|
||||
if (isEnabled) enabled.minus(language) else enabled.plus(language)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ class ToggleSource(
|
||||
}
|
||||
|
||||
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
||||
preferences.disabledSources().getAndSet { disabled ->
|
||||
preferences.disabledSources.getAndSet { disabled ->
|
||||
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
||||
}
|
||||
}
|
||||
|
||||
fun await(sourceIds: List<Long>, enable: Boolean) {
|
||||
val transformedSourceIds = sourceIds.map { it.toString() }
|
||||
preferences.disabledSources().getAndSet { disabled ->
|
||||
preferences.disabledSources.getAndSet { disabled ->
|
||||
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEnabled(sourceId: Long): Boolean {
|
||||
return sourceId.toString() in preferences.disabledSources().get()
|
||||
return sourceId.toString() in preferences.disabledSources.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ class ToggleSourcePin(
|
||||
) {
|
||||
|
||||
fun await(source: Source) {
|
||||
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||
preferences.pinnedSources().getAndSet { pinned ->
|
||||
val isPinned = source.id.toString() in preferences.pinnedSources.get()
|
||||
preferences.pinnedSources.getAndSet { pinned ->
|
||||
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,68 +10,80 @@ import tachiyomi.core.common.preference.getLongArray
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
class SourcePreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||
val sourceDisplayMode: Preference<LibraryDisplayMode> = preferenceStore.getObjectFromString(
|
||||
"pref_display_mode_catalogue",
|
||||
LibraryDisplayMode.default,
|
||||
LibraryDisplayMode.Serializer::serialize,
|
||||
LibraryDisplayMode.Serializer::deserialize,
|
||||
)
|
||||
|
||||
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
||||
val enabledLanguages: Preference<Set<String>> = preferenceStore.getStringSet(
|
||||
"source_languages",
|
||||
LocaleHelper.getDefaultEnabledLanguages(),
|
||||
)
|
||||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
val disabledSources: Preference<Set<String>> = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||
val incognitoExtensions: Preference<Set<String>> = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
val pinnedSources: Preference<Set<String>> = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong(
|
||||
val lastUsedSource: Preference<Long> = preferenceStore.getLong(
|
||||
Preference.appStateKey("last_catalogue_source"),
|
||||
-1,
|
||||
)
|
||||
|
||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
val showNsfwSource: Preference<Boolean> = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
|
||||
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
||||
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
|
||||
"pref_migration_sorting",
|
||||
SetMigrateSorting.Mode.ALPHABETICAL,
|
||||
)
|
||||
|
||||
fun migrationSortingDirection() = preferenceStore.getEnum(
|
||||
val migrationSortingDirection: Preference<SetMigrateSorting.Direction> = preferenceStore.getEnum(
|
||||
"pref_migration_direction",
|
||||
SetMigrateSorting.Direction.ASCENDING,
|
||||
)
|
||||
|
||||
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||
val hideInLibraryItems: Preference<Boolean> = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||
|
||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
val extensionRepos: Preference<Set<String>> = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
|
||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||
val extensionUpdatesCount: Preference<Int> = preferenceStore.getInt("ext_updates_count", 0)
|
||||
|
||||
fun trustedExtensions() = preferenceStore.getStringSet(
|
||||
val trustedExtensions: Preference<Set<String>> = preferenceStore.getStringSet(
|
||||
Preference.appStateKey("trusted_extensions"),
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
fun globalSearchFilterState() = preferenceStore.getBoolean(
|
||||
val globalSearchFilterState: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
Preference.appStateKey("has_filters_toggle_state"),
|
||||
false,
|
||||
)
|
||||
|
||||
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||
val migrationSources: Preference<List<Long>> = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||
|
||||
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||
val migrationFlags: Preference<Set<MigrationFlag>> = preferenceStore.getObjectFromInt(
|
||||
key = "migration_flags",
|
||||
defaultValue = MigrationFlag.entries.toSet(),
|
||||
serializer = { MigrationFlag.toBit(it) },
|
||||
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||
)
|
||||
|
||||
fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false)
|
||||
val migrationDeepSearchMode: Preference<Boolean> = preferenceStore.getBoolean("migration_deep_search", false)
|
||||
|
||||
fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false)
|
||||
val migrationPrioritizeByChapters: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"migration_prioritize_by_chapters",
|
||||
false,
|
||||
)
|
||||
|
||||
fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||
val migrationHideUnmatched: Preference<Boolean> = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||
|
||||
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
|
||||
val migrationHideWithoutUpdates: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"migration_hide_without_updates",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@ class TrackPreferences(
|
||||
|
||||
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
val anilistScoreType: Preference<String> = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
val autoUpdateTrack: Preference<Boolean> = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
|
||||
val autoUpdateTrackOnMarkRead: Preference<AutoTrackState> = preferenceStore.getEnum(
|
||||
"pref_auto_update_manga_on_mark_read",
|
||||
AutoTrackState.ALWAYS,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -12,12 +13,12 @@ import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
class UiPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
||||
val themeMode: Preference<ThemeMode> = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
||||
|
||||
fun appTheme() = preferenceStore.getEnum(
|
||||
val appTheme: Preference<AppTheme> = preferenceStore.getEnum(
|
||||
"pref_app_theme",
|
||||
if (DeviceUtil.isDynamicColorAvailable) {
|
||||
AppTheme.MONET
|
||||
@@ -26,15 +27,15 @@ class UiPreferences(
|
||||
},
|
||||
)
|
||||
|
||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
val themeDarkAmoled: Preference<Boolean> = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
|
||||
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
||||
val relativeTime: Preference<Boolean> = preferenceStore.getBoolean("relative_time_v2", true)
|
||||
|
||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||
val dateFormat: Preference<String> = preferenceStore.getString("app_date_format", "")
|
||||
|
||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||
val tabletUiMode: Preference<TabletUiMode> = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||
|
||||
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||
val imagesInDescription: Preference<Boolean> = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||
|
||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||
onClick = onCreate,
|
||||
|
||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(it.title)
|
||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
||||
// Don't show search action
|
||||
} else if (searchQuery == null) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_search))
|
||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
||||
}
|
||||
} else if (searchQuery.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_reset))
|
||||
|
||||
@@ -22,7 +22,7 @@ fun relativeDateText(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.takeIf { dateEpochMillis > 0L },
|
||||
.takeIf { dateEpochMillis != 0L },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ fun relativeDateText(
|
||||
val context = LocalContext.current
|
||||
|
||||
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
val relativeTime = remember { preferences.relativeTime.get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat.get()) }
|
||||
|
||||
return localDate?.toRelativeString(
|
||||
context = context,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun DownloadDropdownMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
offset: DpOffset? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (offset != null) {
|
||||
DropdownMenu(
|
||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
private fun DownloadDropdownMenuItems(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
) {
|
||||
@@ -59,6 +58,7 @@ private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||
)
|
||||
|
||||
options.map { (downloadAction, string) ->
|
||||
|
||||
@@ -78,9 +78,9 @@ fun LibrarySettingsDialog(
|
||||
private fun ColumnScope.FilterPage(
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
|
||||
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
|
||||
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions().collectAsState()
|
||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded.collectAsState()
|
||||
val downloadedOnly by screenModel.preferences.downloadedOnly.collectAsState()
|
||||
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
|
||||
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
@@ -92,25 +92,25 @@ private fun ColumnScope.FilterPage(
|
||||
enabled = !downloadedOnly,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) },
|
||||
)
|
||||
val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState()
|
||||
val filterUnread by screenModel.libraryPreferences.filterUnread.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
|
||||
)
|
||||
val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState()
|
||||
val filterStarted by screenModel.libraryPreferences.filterStarted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
|
||||
)
|
||||
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState()
|
||||
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
|
||||
)
|
||||
val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState()
|
||||
val filterCompleted by screenModel.libraryPreferences.filterCompleted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.completed),
|
||||
state = filterCompleted,
|
||||
@@ -118,7 +118,7 @@ private fun ColumnScope.FilterPage(
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
state = filterIntervalCustom,
|
||||
@@ -229,7 +229,7 @@ private val displayModes = listOf(
|
||||
private fun ColumnScope.DisplayPage(
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
) {
|
||||
val displayMode by screenModel.libraryPreferences.displayMode().collectAsState()
|
||||
val displayMode by screenModel.libraryPreferences.displayMode.collectAsState()
|
||||
SettingsChipRow(MR.strings.action_display_mode) {
|
||||
displayModes.map { (titleRes, mode) ->
|
||||
FilterChip(
|
||||
@@ -244,9 +244,9 @@ private fun ColumnScope.DisplayPage(
|
||||
val configuration = LocalConfiguration.current
|
||||
val columnPreference = remember {
|
||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
screenModel.libraryPreferences.landscapeColumns()
|
||||
screenModel.libraryPreferences.landscapeColumns
|
||||
} else {
|
||||
screenModel.libraryPreferences.portraitColumns()
|
||||
screenModel.libraryPreferences.portraitColumns
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ private fun ColumnScope.DisplayPage(
|
||||
value = columns,
|
||||
valueRange = 0..10,
|
||||
label = stringResource(MR.strings.pref_library_columns),
|
||||
valueText = if (columns > 0) {
|
||||
valueString = if (columns > 0) {
|
||||
columns.toString()
|
||||
} else {
|
||||
stringResource(MR.strings.label_auto)
|
||||
@@ -268,32 +268,32 @@ private fun ColumnScope.DisplayPage(
|
||||
HeadingItem(MR.strings.overlay_header)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_download_badge),
|
||||
pref = screenModel.libraryPreferences.downloadBadge(),
|
||||
pref = screenModel.libraryPreferences.downloadBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_unread_badge),
|
||||
pref = screenModel.libraryPreferences.unreadBadge(),
|
||||
pref = screenModel.libraryPreferences.unreadBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_local_badge),
|
||||
pref = screenModel.libraryPreferences.localBadge(),
|
||||
pref = screenModel.libraryPreferences.localBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_language_badge),
|
||||
pref = screenModel.libraryPreferences.languageBadge(),
|
||||
pref = screenModel.libraryPreferences.languageBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_continue_reading_button),
|
||||
pref = screenModel.libraryPreferences.showContinueReadingButton(),
|
||||
pref = screenModel.libraryPreferences.showContinueReadingButton,
|
||||
)
|
||||
|
||||
HeadingItem(MR.strings.tabs_header)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_tabs),
|
||||
pref = screenModel.libraryPreferences.categoryTabs(),
|
||||
pref = screenModel.libraryPreferences.categoryTabs,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_number_of_items),
|
||||
pref = screenModel.libraryPreferences.categoryNumberOfItems(),
|
||||
pref = screenModel.libraryPreferences.categoryNumberOfItems,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ fun ChapterSettingsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
|
||||
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly.get() }
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -27,9 +24,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -71,7 +70,6 @@ import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -124,7 +122,7 @@ fun MangaScreen(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -255,7 +253,7 @@ private fun MangaScreenSmallImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -330,25 +328,23 @@ private fun MangaScreenSmallImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
@@ -499,7 +495,7 @@ fun MangaScreenLargeImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -572,27 +568,25 @@ fun MangaScreenLargeImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
PullRefresh(
|
||||
@@ -745,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
) {
|
||||
items(
|
||||
@@ -793,14 +787,14 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
onChapterSelected(item, !item.selected, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
||||
NEXT_10_CHAPTERS,
|
||||
NEXT_25_CHAPTERS,
|
||||
UNREAD_CHAPTERS,
|
||||
BOOKMARKED_CHAPTERS,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
|
||||
@@ -91,10 +91,10 @@ fun MangaBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
@@ -252,10 +252,10 @@ fun LibraryBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -68,6 +69,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -565,44 +567,47 @@ private fun ColumnScope.MangaContentInfo(
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
@Composable
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||
markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
@@ -613,11 +618,12 @@ private fun MangaSummary(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val preferences = remember { Injekt.get<UiPreferences>() }
|
||||
val loadImages = remember { preferences.imagesInDescription().get() }
|
||||
val loadImages = remember { preferences.imagesInDescription.get() }
|
||||
val animProgress by animateFloatAsState(
|
||||
targetValue = if (expanded) 1f else 0f,
|
||||
label = "summary",
|
||||
)
|
||||
var infoHeight by remember { mutableIntStateOf(0) }
|
||||
Layout(
|
||||
modifier = modifier.clipToBounds(),
|
||||
contents = listOf(
|
||||
@@ -630,25 +636,11 @@ private fun MangaSummary(
|
||||
)
|
||||
},
|
||||
{
|
||||
Column {
|
||||
MangaNotesSection(
|
||||
content = notes,
|
||||
expanded = true,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
MarkdownRender(
|
||||
content = description,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
annotator = descriptionAnnotator(
|
||||
loadImages = loadImages,
|
||||
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
|
||||
),
|
||||
loadImages = loadImages,
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.onSizeChanged { size ->
|
||||
infoHeight = size.height
|
||||
},
|
||||
) {
|
||||
MangaNotesSection(
|
||||
content = notes,
|
||||
expanded = expanded,
|
||||
@@ -685,14 +677,11 @@ private fun MangaSummary(
|
||||
}
|
||||
},
|
||||
),
|
||||
) { (shrunk, expanded, actual, scrim), constraints ->
|
||||
) { (shrunk, actual, scrim), constraints ->
|
||||
val shrunkHeight = shrunk.single()
|
||||
.measure(constraints)
|
||||
.height
|
||||
val expandedHeight = expanded.single()
|
||||
.measure(constraints)
|
||||
.height
|
||||
val heightDelta = expandedHeight - shrunkHeight
|
||||
val heightDelta = infoHeight - shrunkHeight
|
||||
val scrimHeight = 24.dp.roundToPx()
|
||||
|
||||
val actualPlaceable = actual.single()
|
||||
|
||||
@@ -102,13 +102,9 @@ private fun getMarkdownColors(): MarkdownColors {
|
||||
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
return DefaultMarkdownColors(
|
||||
text = MaterialTheme.colorScheme.onSurface,
|
||||
codeText = Color.Unspecified,
|
||||
inlineCodeText = Color.Unspecified,
|
||||
linkText = Color.Unspecified,
|
||||
codeBackground = codeBackground,
|
||||
inlineCodeBackground = codeBackground,
|
||||
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||
tableText = Color.Unspecified,
|
||||
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
|
||||
)
|
||||
}
|
||||
@@ -139,7 +135,6 @@ private fun getMarkdownTypography(): MarkdownTypography {
|
||||
ordered = MaterialTheme.typography.bodyMedium,
|
||||
bullet = MaterialTheme.typography.bodyMedium,
|
||||
list = MaterialTheme.typography.bodyMedium,
|
||||
link = link,
|
||||
textLink = TextLinkStyles(style = link.toSpanStyle()),
|
||||
table = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
@@ -125,7 +125,7 @@ internal class PermissionStep : OnboardingStep {
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
|
||||
val crashlyticsPref = privacyPreferences.crashlytics()
|
||||
val crashlyticsPref = privacyPreferences.crashlytics
|
||||
val crashlytics by crashlyticsPref.collectAsState()
|
||||
PermissionSwitch(
|
||||
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||
@@ -134,7 +134,7 @@ internal class PermissionStep : OnboardingStep {
|
||||
onToggleChange = crashlyticsPref::set,
|
||||
)
|
||||
|
||||
val analyticsPref = privacyPreferences.analytics()
|
||||
val analyticsPref = privacyPreferences.analytics
|
||||
val analytics by analyticsPref.collectAsState()
|
||||
PermissionSwitch(
|
||||
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||
|
||||
@@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
internal class StorageStep : OnboardingStep {
|
||||
|
||||
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
|
||||
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory
|
||||
|
||||
private var _isComplete by mutableStateOf(false)
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ internal class ThemeStep : OnboardingStep {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeModePref = uiPreferences.themeMode
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appThemePref = uiPreferences.appTheme
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoledPref = uiPreferences.themeDarkAmoled
|
||||
val amoled by amoledPref.collectAsState()
|
||||
|
||||
Column {
|
||||
|
||||
@@ -15,10 +15,10 @@ sealed class Preference {
|
||||
abstract val title: String
|
||||
abstract val enabled: Boolean
|
||||
|
||||
sealed class PreferenceItem<T> : Preference() {
|
||||
sealed class PreferenceItem<T, R> : Preference() {
|
||||
abstract val subtitle: String?
|
||||
abstract val icon: ImageVector?
|
||||
abstract val onValueChanged: suspend (value: T) -> Boolean
|
||||
abstract val onValueChanged: suspend (value: T) -> R
|
||||
|
||||
/**
|
||||
* A basic [PreferenceItem] that only displays texts.
|
||||
@@ -27,10 +27,11 @@ sealed class Preference {
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val enabled: Boolean = true,
|
||||
val widget: @Composable (() -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : PreferenceItem<String>() {
|
||||
) : PreferenceItem<String, Unit>() {
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ sealed class Preference {
|
||||
override val subtitle: String? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>() {
|
||||
) : PreferenceItem<Boolean, Boolean>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
@@ -52,12 +53,13 @@ sealed class Preference {
|
||||
data class SliderPreference(
|
||||
val value: Int,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
val valueString: String? = null,
|
||||
val valueRange: IntProgression = 0..1,
|
||||
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
||||
override val subtitle: String? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
|
||||
) : PreferenceItem<Int>() {
|
||||
override val onValueChanged: suspend (value: Int) -> Unit = {},
|
||||
) : PreferenceItem<Int, Unit>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ sealed class Preference {
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
||||
) : PreferenceItem<T>() {
|
||||
) : PreferenceItem<T, Boolean>() {
|
||||
internal fun internalSet(value: Any) = preference.set(value as T)
|
||||
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
||||
|
||||
@@ -96,8 +98,8 @@ sealed class Preference {
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
override val onValueChanged: suspend (value: String) -> Unit = {},
|
||||
) : PreferenceItem<String, Unit>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
@@ -121,7 +123,7 @@ sealed class Preference {
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<String>>()
|
||||
) : PreferenceItem<Set<String>, Boolean>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
@@ -132,7 +134,7 @@ sealed class Preference {
|
||||
override val subtitle: String? = "%s",
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>() {
|
||||
) : PreferenceItem<String, Boolean>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
@@ -143,31 +145,31 @@ sealed class Preference {
|
||||
val tracker: Tracker,
|
||||
val login: () -> Unit,
|
||||
val logout: () -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
) : PreferenceItem<String, Unit>() {
|
||||
override val title: String = ""
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||
}
|
||||
|
||||
data class InfoPreference(
|
||||
override val title: String,
|
||||
) : PreferenceItem<String>() {
|
||||
) : PreferenceItem<String, Unit>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Unit = {}
|
||||
}
|
||||
|
||||
data class CustomPreference(
|
||||
override val title: String,
|
||||
val content: @Composable () -> Unit,
|
||||
) : PreferenceItem<Unit>() {
|
||||
) : PreferenceItem<Unit, Unit>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: Unit) -> Unit = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +177,6 @@ sealed class Preference {
|
||||
override val title: String,
|
||||
override val enabled: Boolean = true,
|
||||
|
||||
val preferenceItems: ImmutableList<PreferenceItem<out Any>>,
|
||||
val preferenceItems: ImmutableList<PreferenceItem<out Any, out Any>>,
|
||||
) : Preference()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) {
|
||||
|
||||
@Composable
|
||||
fun StatusWrapper(
|
||||
item: Preference.PreferenceItem<*>,
|
||||
item: Preference.PreferenceItem<*, *>,
|
||||
highlightKey: String?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
@@ -56,7 +56,7 @@ fun StatusWrapper(
|
||||
|
||||
@Composable
|
||||
internal fun PreferenceItem(
|
||||
item: Preference.PreferenceItem<*>,
|
||||
item: Preference.PreferenceItem<*, *>,
|
||||
highlightKey: String?,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -83,17 +83,18 @@ internal fun PreferenceItem(
|
||||
}
|
||||
is Preference.PreferenceItem.SliderPreference -> {
|
||||
BaseSliderItem(
|
||||
label = item.title,
|
||||
value = item.value,
|
||||
valueRange = item.valueRange,
|
||||
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||
steps = item.steps,
|
||||
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||
onChange = {
|
||||
scope.launch {
|
||||
item.onValueChanged(it)
|
||||
}
|
||||
},
|
||||
titleStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = PrefsHorizontalPadding,
|
||||
vertical = PrefsVerticalPadding,
|
||||
@@ -146,6 +147,7 @@ internal fun PreferenceItem(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
widget = item.widget,
|
||||
onPreferenceClick = item.onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fun PreferenceScreen(
|
||||
}
|
||||
|
||||
// Create Preference Item
|
||||
is Preference.PreferenceItem<*> -> item {
|
||||
is Preference.PreferenceItem<*, *> -> item {
|
||||
PreferenceItem(
|
||||
item = preference,
|
||||
highlightKey = highlightKey,
|
||||
|
||||
+14
-9
@@ -99,7 +99,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = networkPreferences.verboseLogging(),
|
||||
preference = networkPreferences.verboseLogging,
|
||||
title = stringResource(MR.strings.pref_verbose_logging),
|
||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
@@ -204,7 +204,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
val context = LocalContext.current
|
||||
val networkHelper = remember { Injekt.get<NetworkHelper>() }
|
||||
|
||||
val userAgentPref = networkPreferences.defaultUserAgent()
|
||||
val userAgentPref = networkPreferences.defaultUserAgent
|
||||
val userAgent by userAgentPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
@@ -238,7 +238,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = networkPreferences.dohProvider(),
|
||||
preference = networkPreferences.dohProvider,
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
@@ -319,10 +319,15 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.updateMangaTitles(),
|
||||
preference = libraryPreferences.updateMangaTitles,
|
||||
title = stringResource(MR.strings.pref_update_library_manga_titles),
|
||||
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.disallowNonAsciiFilenames,
|
||||
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
|
||||
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -338,14 +343,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
uri?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
basePreferences.displayProfile().set(uri.toString())
|
||||
basePreferences.displayProfile.set(uri.toString())
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = basePreferences.hardwareBitmapThreshold(),
|
||||
preference = basePreferences.hardwareBitmapThreshold,
|
||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||
.mapIndexed { index, option ->
|
||||
val display = if (index == 0) {
|
||||
@@ -365,13 +370,13 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
|
||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV,
|
||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
|
||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile().get(),
|
||||
subtitle = basePreferences.displayProfile.get(),
|
||||
onClick = {
|
||||
chooseColorProfile.launch(arrayOf("*/*"))
|
||||
},
|
||||
@@ -386,7 +391,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val extensionInstallerPref = basePreferences.extensionInstaller()
|
||||
val extensionInstallerPref = basePreferences.extensionInstaller
|
||||
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
|
||||
val trustExtension = remember { Injekt.get<TrustExtension>() }
|
||||
|
||||
|
||||
+8
-8
@@ -50,13 +50,13 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeModePref = uiPreferences.themeMode
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appThemePref = uiPreferences.appTheme
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoledPref = uiPreferences.themeDarkAmoled
|
||||
val amoled by amoledPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
@@ -103,7 +103,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
|
||||
val now = remember { LocalDate.now() }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||
val dateFormat by uiPreferences.dateFormat.collectAsState()
|
||||
val formattedNow = remember(dateFormat) {
|
||||
UiPreferences.dateFormat(dateFormat).format(now)
|
||||
}
|
||||
@@ -116,7 +116,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
onClick = { navigator.push(AppLanguageScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = uiPreferences.tabletUiMode(),
|
||||
preference = uiPreferences.tabletUiMode,
|
||||
entries = TabletUiMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
@@ -127,7 +127,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = uiPreferences.dateFormat(),
|
||||
preference = uiPreferences.dateFormat,
|
||||
entries = DateFormats
|
||||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
@@ -137,7 +137,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = uiPreferences.relativeTime(),
|
||||
preference = uiPreferences.relativeTime,
|
||||
title = stringResource(MR.strings.pref_relative_format),
|
||||
subtitle = stringResource(
|
||||
MR.strings.pref_relative_format_summary,
|
||||
@@ -146,7 +146,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = uiPreferences.imagesInDescription(),
|
||||
preference = uiPreferences.imagesInDescription,
|
||||
title = stringResource(MR.strings.pref_display_images_description),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = sourcePreferences.hideInLibraryItems(),
|
||||
preference = sourcePreferences.hideInLibraryItems,
|
||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
@@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = sourcePreferences.showNsfwSource(),
|
||||
preference = sourcePreferences.showNsfwSource,
|
||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||
subtitle = stringResource(MR.strings.requires_app_restart),
|
||||
onValueChanged = {
|
||||
|
||||
@@ -167,11 +167,11 @@ object SettingsDataScreen : SearchableSettings {
|
||||
storagePreferences: StoragePreferences,
|
||||
): Preference.PreferenceItem.TextPreference {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = storageLocationPicker(storagePreferences.baseStorageDirectory())
|
||||
val pickStorageLocation = storageLocationPicker(storagePreferences.baseStorageDirectory)
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_storage_location),
|
||||
subtitle = storageLocationText(storagePreferences.baseStorageDirectory()),
|
||||
subtitle = storageLocationText(storagePreferences.baseStorageDirectory),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
@@ -187,7 +187,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp.collectAsState()
|
||||
|
||||
val chooseBackup = rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
@@ -254,7 +254,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
// Automatic backups
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = backupPreferences.backupInterval(),
|
||||
preference = backupPreferences.backupInterval,
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
6 to stringResource(MR.strings.update_6hour),
|
||||
@@ -321,7 +321,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.autoClearChapterCache(),
|
||||
preference = libraryPreferences.autoClearChapterCache,
|
||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
),
|
||||
|
||||
+27
-12
@@ -37,20 +37,35 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
val parallelSourceLimit by downloadPreferences.parallelSourceLimit.collectAsState()
|
||||
val parallelPageLimit by downloadPreferences.parallelPageLimit.collectAsState()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.downloadOnlyOverWifi(),
|
||||
preference = downloadPreferences.downloadOnlyOverWifi,
|
||||
title = stringResource(MR.strings.connected_to_wifi),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.saveChaptersAsCBZ(),
|
||||
preference = downloadPreferences.saveChaptersAsCBZ,
|
||||
title = stringResource(MR.strings.save_chapter_as_cbz),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.splitTallImages(),
|
||||
preference = downloadPreferences.splitTallImages,
|
||||
title = stringResource(MR.strings.split_tall_images),
|
||||
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = parallelSourceLimit,
|
||||
valueRange = 1..10,
|
||||
title = stringResource(MR.strings.pref_download_concurrent_sources),
|
||||
onValueChanged = { downloadPreferences.parallelSourceLimit.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = parallelPageLimit,
|
||||
valueRange = 1..15,
|
||||
title = stringResource(MR.strings.pref_download_concurrent_pages),
|
||||
subtitle = stringResource(MR.strings.pref_download_concurrent_pages_summary),
|
||||
onValueChanged = { downloadPreferences.parallelPageLimit.set(it) },
|
||||
),
|
||||
getDeleteChaptersGroup(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = allCategories,
|
||||
@@ -72,11 +87,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
preference = downloadPreferences.removeAfterMarkedAsRead,
|
||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = downloadPreferences.removeAfterReadSlots(),
|
||||
preference = downloadPreferences.removeAfterReadSlots,
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
0 to stringResource(MR.strings.last_read_chapter),
|
||||
@@ -88,7 +103,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = downloadPreferences.removeBookmarkedChapters(),
|
||||
preference = downloadPreferences.removeBookmarkedChapters,
|
||||
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
||||
),
|
||||
getExcludedCategoriesPreference(
|
||||
@@ -105,7 +120,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = downloadPreferences.removeExcludeCategories(),
|
||||
preference = downloadPreferences.removeExcludeCategories,
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
@@ -118,10 +133,10 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
downloadPreferences: DownloadPreferences,
|
||||
allCategories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters()
|
||||
val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly()
|
||||
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories()
|
||||
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude()
|
||||
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters
|
||||
val downloadNewUnreadChaptersOnlyPref = downloadPreferences.downloadNewUnreadChaptersOnly
|
||||
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories
|
||||
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude
|
||||
|
||||
val downloadNewChapters by downloadNewChaptersPref.collectAsState()
|
||||
|
||||
@@ -179,7 +194,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.download_ahead),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = downloadPreferences.autoDownloadWhileReading(),
|
||||
preference = downloadPreferences.autoDownloadWhileReading,
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
|
||||
+14
-14
@@ -73,7 +73,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||
|
||||
// For default category
|
||||
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
||||
val ids = listOf(libraryPreferences.defaultCategory.defaultValue()) +
|
||||
allCategories.fastMap { it.id.toInt() }
|
||||
val labels = listOf(stringResource(MR.strings.default_category_summary)) +
|
||||
allCategories.fastMap { it.visualName }
|
||||
@@ -91,12 +91,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { navigator.push(CategoryScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = libraryPreferences.defaultCategory(),
|
||||
preference = libraryPreferences.defaultCategory,
|
||||
entries = ids.zip(labels).toMap().toImmutableMap(),
|
||||
title = stringResource(MR.strings.default_category),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.categorizedDisplaySettings(),
|
||||
preference = libraryPreferences.categorizedDisplaySettings,
|
||||
title = stringResource(MR.strings.categorized_display_settings),
|
||||
onValueChanged = {
|
||||
if (!it) {
|
||||
@@ -118,9 +118,9 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
|
||||
val autoUpdateIntervalPref = libraryPreferences.autoUpdateInterval()
|
||||
val autoUpdateCategoriesPref = libraryPreferences.updateCategories()
|
||||
val autoUpdateCategoriesExcludePref = libraryPreferences.updateCategoriesExclude()
|
||||
val autoUpdateIntervalPref = libraryPreferences.autoUpdateInterval
|
||||
val autoUpdateCategoriesPref = libraryPreferences.updateCategories
|
||||
val autoUpdateCategoriesExcludePref = libraryPreferences.updateCategoriesExclude
|
||||
|
||||
val autoUpdateInterval by autoUpdateIntervalPref.collectAsState()
|
||||
|
||||
@@ -164,7 +164,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||
preference = libraryPreferences.autoUpdateDeviceRestrictions,
|
||||
entries = persistentMapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||
@@ -189,12 +189,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { showCategoriesDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.autoUpdateMetadata(),
|
||||
preference = libraryPreferences.autoUpdateMetadata,
|
||||
title = stringResource(MR.strings.pref_library_update_refresh_metadata),
|
||||
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||
preference = libraryPreferences.autoUpdateMangaRestrictions,
|
||||
entries = persistentMapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
||||
@@ -204,7 +204,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.newShowUpdatesCount(),
|
||||
preference = libraryPreferences.newShowUpdatesCount,
|
||||
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
|
||||
),
|
||||
),
|
||||
@@ -219,7 +219,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_behavior),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = libraryPreferences.swipeToStartAction(),
|
||||
preference = libraryPreferences.swipeToStartAction,
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -233,7 +233,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = libraryPreferences.swipeToEndAction(),
|
||||
preference = libraryPreferences.swipeToEndAction,
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -247,7 +247,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
|
||||
preference = libraryPreferences.markDuplicateReadChapterAsRead,
|
||||
entries = persistentMapOf(
|
||||
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
|
||||
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
|
||||
@@ -257,7 +257,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.hideMissingChapters(),
|
||||
preference = libraryPreferences.hideMissingChapters,
|
||||
title = stringResource(MR.strings.pref_hide_missing_chapter_indicators),
|
||||
),
|
||||
),
|
||||
|
||||
+54
-65
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -10,6 +9,7 @@ import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
@@ -33,14 +33,14 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPref.defaultReadingMode(),
|
||||
preference = readerPref.defaultReadingMode,
|
||||
entries = ReadingMode.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPref.doubleTapAnimSpeed(),
|
||||
preference = readerPref.doubleTapAnimSpeed,
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||
@@ -49,17 +49,17 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPref.showReadingMode(),
|
||||
preference = readerPref.showReadingMode,
|
||||
title = stringResource(MR.strings.pref_show_reading_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPref.showNavigationOverlayOnStart(),
|
||||
preference = readerPref.showNavigationOverlayOnStart,
|
||||
title = stringResource(MR.strings.pref_show_navigation_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPref.pageTransitions(),
|
||||
preference = readerPref.pageTransitions,
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
),
|
||||
getDisplayGroup(readerPreferences = readerPref),
|
||||
@@ -74,20 +74,20 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val fullscreenPref = readerPreferences.fullscreen()
|
||||
val fullscreenPref = readerPreferences.fullscreen
|
||||
val fullscreen by fullscreenPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.defaultOrientationType(),
|
||||
preference = readerPreferences.defaultOrientationType,
|
||||
entries = ReaderOrientation.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.readerTheme(),
|
||||
preference = readerPreferences.readerTheme,
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.black_background),
|
||||
2 to stringResource(MR.strings.gray_background),
|
||||
@@ -101,18 +101,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_fullscreen),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.cutoutShort(),
|
||||
preference = readerPreferences.drawUnderCutout,
|
||||
title = stringResource(MR.strings.pref_cutout_short),
|
||||
enabled = fullscreen &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
||||
enabled = LocalView.current.hasDisplayCutout() && fullscreen,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.keepScreenOn(),
|
||||
preference = readerPreferences.keepScreenOn,
|
||||
title = stringResource(MR.strings.pref_keep_screen_on),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.showPageNumber(),
|
||||
preference = readerPreferences.showPageNumber,
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
),
|
||||
@@ -121,21 +119,21 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
private fun getEInkGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val flashPageState by readerPreferences.flashOnPageChange().collectAsState()
|
||||
val flashPageState by readerPreferences.flashOnPageChange.collectAsState()
|
||||
|
||||
val flashMillisPref = readerPreferences.flashDurationMillis()
|
||||
val flashMillisPref = readerPreferences.flashDurationMillis
|
||||
val flashMillis by flashMillisPref.collectAsState()
|
||||
|
||||
val flashIntervalPref = readerPreferences.flashPageInterval()
|
||||
val flashIntervalPref = readerPreferences.flashPageInterval
|
||||
val flashInterval by flashIntervalPref.collectAsState()
|
||||
|
||||
val flashColorPref = readerPreferences.flashColor()
|
||||
val flashColorPref = readerPreferences.flashColor
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = "E-Ink",
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.flashOnPageChange(),
|
||||
preference = readerPreferences.flashOnPageChange,
|
||||
title = stringResource(MR.strings.pref_flash_page),
|
||||
subtitle = stringResource(MR.strings.pref_flash_page_summ),
|
||||
),
|
||||
@@ -143,23 +141,17 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
valueRange = 1..15,
|
||||
title = stringResource(MR.strings.pref_flash_duration),
|
||||
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
||||
true
|
||||
},
|
||||
onValueChanged = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = flashInterval,
|
||||
valueRange = 1..10,
|
||||
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashIntervalPref.set(it)
|
||||
true
|
||||
},
|
||||
onValueChanged = { flashIntervalPref.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = flashColorPref,
|
||||
@@ -182,19 +174,19 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_reading),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.skipRead(),
|
||||
preference = readerPreferences.skipRead,
|
||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.skipFiltered(),
|
||||
preference = readerPreferences.skipFiltered,
|
||||
title = stringResource(MR.strings.pref_skip_filtered_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.skipDupe(),
|
||||
preference = readerPreferences.skipDupe,
|
||||
title = stringResource(MR.strings.pref_skip_dupe_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.alwaysShowChapterTransition(),
|
||||
preference = readerPreferences.alwaysShowChapterTransition,
|
||||
title = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
),
|
||||
),
|
||||
@@ -203,10 +195,10 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
private fun getPagedGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val navModePref = readerPreferences.navigationModePager()
|
||||
val imageScaleTypePref = readerPreferences.imageScaleType()
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitPaged()
|
||||
val rotateToFitPref = readerPreferences.dualPageRotateToFit()
|
||||
val navModePref = readerPreferences.navigationModePager
|
||||
val imageScaleTypePref = readerPreferences.imageScaleType
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitPaged
|
||||
val rotateToFitPref = readerPreferences.dualPageRotateToFit
|
||||
|
||||
val navMode by navModePref.collectAsState()
|
||||
val imageScaleType by imageScaleTypePref.collectAsState()
|
||||
@@ -225,7 +217,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.pagerNavInverted(),
|
||||
preference = readerPreferences.pagerNavInverted,
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -246,7 +238,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.zoomStart(),
|
||||
preference = readerPreferences.zoomStart,
|
||||
entries = ReaderPreferences.ZoomStart
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap()
|
||||
@@ -254,16 +246,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.cropBorders(),
|
||||
preference = readerPreferences.cropBorders,
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.landscapeZoom(),
|
||||
preference = readerPreferences.landscapeZoom,
|
||||
title = stringResource(MR.strings.pref_landscape_zoom),
|
||||
enabled = imageScaleType == 1,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.navigateToPan(),
|
||||
preference = readerPreferences.navigateToPan,
|
||||
title = stringResource(MR.strings.pref_navigate_pan),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
@@ -276,7 +268,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.dualPageInvertPaged(),
|
||||
preference = readerPreferences.dualPageInvertPaged,
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
@@ -290,7 +282,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.dualPageRotateToFitInvert(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvert,
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
@@ -302,10 +294,10 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
private fun getWebtoonGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val numberFormat = remember { NumberFormat.getPercentInstance() }
|
||||
|
||||
val navModePref = readerPreferences.navigationModeWebtoon()
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon()
|
||||
val rotateToFitPref = readerPreferences.dualPageRotateToFitWebtoon()
|
||||
val webtoonSidePaddingPref = readerPreferences.webtoonSidePadding()
|
||||
val navModePref = readerPreferences.navigationModeWebtoon
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon
|
||||
val rotateToFitPref = readerPreferences.dualPageRotateToFitWebtoon
|
||||
val webtoonSidePaddingPref = readerPreferences.webtoonSidePadding
|
||||
|
||||
val navMode by navModePref.collectAsState()
|
||||
val dualPageSplit by dualPageSplitPref.collectAsState()
|
||||
@@ -324,7 +316,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.webtoonNavInverted(),
|
||||
preference = readerPreferences.webtoonNavInverted,
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -342,14 +334,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
||||
},
|
||||
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
subtitle = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onValueChanged = {
|
||||
webtoonSidePaddingPref.set(it)
|
||||
true
|
||||
},
|
||||
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onValueChanged = { webtoonSidePaddingPref.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = readerPreferences.readerHideThreshold(),
|
||||
preference = readerPreferences.readerHideThreshold,
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||
@@ -359,7 +348,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.cropBordersWebtoon(),
|
||||
preference = readerPreferences.cropBordersWebtoon,
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
@@ -371,7 +360,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.dualPageInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageInvertWebtoon,
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
@@ -385,16 +374,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvertWebtoon,
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
preference = readerPreferences.webtoonDoubleTapZoomEnabled,
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.webtoonDisableZoomOut(),
|
||||
preference = readerPreferences.webtoonDisableZoomOut,
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
),
|
||||
@@ -403,7 +392,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
private fun getNavigationGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys()
|
||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys
|
||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
@@ -413,7 +402,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.readWithVolumeKeysInverted(),
|
||||
preference = readerPreferences.readWithVolumeKeysInverted,
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
@@ -427,11 +416,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_reader_actions),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.readWithLongTap(),
|
||||
preference = readerPreferences.readWithLongTap,
|
||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.folderPerManga(),
|
||||
preference = readerPreferences.folderPerManga,
|
||||
title = stringResource(MR.strings.pref_create_folder_per_manga),
|
||||
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
|
||||
),
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ private fun SearchResult(
|
||||
emptySequence()
|
||||
}
|
||||
}
|
||||
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
|
||||
is Preference.PreferenceItem<*, *> -> sequenceOf(null to p)
|
||||
}
|
||||
}
|
||||
// Don't show info preference
|
||||
|
||||
+6
-6
@@ -45,7 +45,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val authSupported = remember { context.isAuthenticationSupported() }
|
||||
val useAuthPref = securityPreferences.useAuthenticator()
|
||||
val useAuthPref = securityPreferences.useAuthenticator
|
||||
val useAuth by useAuthPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
@@ -62,7 +62,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = securityPreferences.lockAppAfter(),
|
||||
preference = securityPreferences.lockAppAfter,
|
||||
entries = LockAfterValues
|
||||
.associateWith {
|
||||
when (it) {
|
||||
@@ -82,11 +82,11 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
),
|
||||
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = securityPreferences.hideNotificationContent(),
|
||||
preference = securityPreferences.hideNotificationContent,
|
||||
title = stringResource(MR.strings.hide_notification_content),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = securityPreferences.secureScreen(),
|
||||
preference = securityPreferences.secureScreen,
|
||||
entries = SecurityPreferences.SecureScreenMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
@@ -105,12 +105,12 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_firebase),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = privacyPreferences.crashlytics(),
|
||||
preference = privacyPreferences.crashlytics,
|
||||
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = privacyPreferences.analytics(),
|
||||
preference = privacyPreferences.analytics,
|
||||
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
|
||||
),
|
||||
|
||||
+3
-3
@@ -90,7 +90,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||
val trackerManager = remember { Injekt.get<TrackerManager>() }
|
||||
val sourceManager = remember { Injekt.get<SourceManager>() }
|
||||
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
|
||||
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead
|
||||
|
||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||
dialog?.run {
|
||||
@@ -128,11 +128,11 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = trackPreferences.autoUpdateTrack(),
|
||||
preference = trackPreferences.autoUpdateTrack,
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
preference = trackPreferences.autoUpdateTrackOnMarkRead,
|
||||
entries = AutoTrackState.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toPersistentMap(),
|
||||
|
||||
@@ -225,7 +225,6 @@ object AboutScreen : Screen() {
|
||||
is GetApplicationRelease.Result.OsTooOld -> {
|
||||
context.toast(MR.strings.update_check_eol)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
@@ -276,7 +275,7 @@ object AboutScreen : Screen() {
|
||||
)
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
Injekt.get<UiPreferences>().dateFormat.get(),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+5
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||
LibrariesContainer(
|
||||
libraries = libraries,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
|
||||
+4
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -27,6 +28,7 @@ class ExtensionReposScreenModel(
|
||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
@@ -53,6 +55,7 @@ class ExtensionReposScreenModel(
|
||||
fun createRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
|
||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||
@@ -93,6 +96,7 @@ class ExtensionReposScreenModel(
|
||||
fun deleteRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
deleteExtensionRepo.await(baseUrl)
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -57,10 +58,11 @@ fun ExtensionRepoCreateDialog(
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(MR.strings.action_add_repo_message))
|
||||
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
|
||||
+43
-1
@@ -1,24 +1,38 @@
|
||||
package eu.kanade.presentation.more.settings.screen.debug
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Autorenew
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.profileinstaller.ProfileVerifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.PreferenceScaffold
|
||||
import eu.kanade.presentation.more.settings.screen.about.AboutScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.core.common.FeatureFlags
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DebugInfoScreen : Screen() {
|
||||
|
||||
@@ -47,6 +61,12 @@ class DebugInfoScreen : Screen() {
|
||||
|
||||
@Composable
|
||||
private fun getAppInfoGroup(): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val installationIdPref = remember { Injekt.get<BasePreferences>().installationId }
|
||||
val installationId by installationIdPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = "App info",
|
||||
preferenceItems = persistentListOf(
|
||||
@@ -58,6 +78,28 @@ class DebugInfoScreen : Screen() {
|
||||
title = "Build time",
|
||||
subtitle = AboutScreen.getFormattedBuildTime(),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Installation ID",
|
||||
subtitle = installationId,
|
||||
widget = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
installationIdPref.set(FeatureFlags.newInstallationId())
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Autorenew,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
context.copyToClipboard(installationId, installationId)
|
||||
},
|
||||
),
|
||||
getProfileVerifierPreference(),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "WebView version",
|
||||
@@ -99,7 +141,7 @@ class DebugInfoScreen : Screen() {
|
||||
}
|
||||
|
||||
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
|
||||
val items = persistentListOf<Preference.PreferenceItem<out Any>>().mutate {
|
||||
val items = persistentListOf<Preference.PreferenceItem<out Any, out Any>>().mutate {
|
||||
it.add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Model",
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ class WorkerInfoScreen : Screen() {
|
||||
)
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
Injekt.get<UiPreferences>().dateFormat.get(),
|
||||
),
|
||||
)
|
||||
appendLine("Next scheduled run: $timestamp")
|
||||
|
||||
@@ -24,10 +24,10 @@ class DisplayRefreshHost {
|
||||
internal var currentDisplayRefresh by mutableStateOf(false)
|
||||
private val readerPreferences = Injekt.get<ReaderPreferences>()
|
||||
|
||||
internal val flashMillis = readerPreferences.flashDurationMillis()
|
||||
internal val flashMode = readerPreferences.flashColor()
|
||||
internal val flashMillis = readerPreferences.flashDurationMillis
|
||||
internal val flashMode = readerPreferences.flashColor
|
||||
|
||||
internal val flashIntervalPref = readerPreferences.flashPageInterval()
|
||||
internal val flashIntervalPref = readerPreferences.flashPageInterval
|
||||
|
||||
// Internal State for Flash
|
||||
private var flashInterval = flashIntervalPref.get()
|
||||
|
||||
+6
-3
@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -15,9 +16,10 @@ import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
|
||||
@Composable
|
||||
fun PageIndicatorText(
|
||||
fun ReaderPageIndicator(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (currentPage <= 0 || totalPages <= 0) return
|
||||
|
||||
@@ -36,6 +38,7 @@ fun PageIndicatorText(
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -50,10 +53,10 @@ fun PageIndicatorText(
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun PageIndicatorTextPreview() {
|
||||
private fun ReaderPageIndicatorPreview() {
|
||||
TachiyomiPreviewTheme {
|
||||
Surface {
|
||||
PageIndicatorText(currentPage = 10, totalPages = 69)
|
||||
ReaderPageIndicator(currentPage = 10, totalPages = 69)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,42 +2,41 @@ package eu.kanade.presentation.reader.appbars
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.BookmarkBorder
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.reader.components.ChapterNavigator
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
private val animationSpec = tween<IntOffset>(200)
|
||||
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
|
||||
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
||||
|
||||
@Composable
|
||||
fun ReaderAppBars(
|
||||
visible: Boolean,
|
||||
fullscreen: Boolean,
|
||||
|
||||
mangaTitle: String?,
|
||||
chapterTitle: String?,
|
||||
@@ -71,83 +70,26 @@ fun ReaderAppBars(
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
||||
|
||||
val modifierWithInsetsPadding = if (fullscreen) {
|
||||
Modifier.systemBarsPadding()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxHeight()) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { -it },
|
||||
animationSpec = animationSpec,
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { -it },
|
||||
animationSpec = animationSpec,
|
||||
),
|
||||
enter = slideInVertically(initialOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||
) {
|
||||
AppBar(
|
||||
modifier = modifierWithInsetsPadding
|
||||
ReaderTopBar(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.clickable(onClick = onClickTopAppBar),
|
||||
backgroundColor = backgroundColor,
|
||||
title = mangaTitle,
|
||||
subtitle = chapterTitle,
|
||||
mangaTitle = mangaTitle,
|
||||
chapterTitle = chapterTitle,
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(
|
||||
if (bookmarked) {
|
||||
MR.strings.action_remove_bookmark
|
||||
} else {
|
||||
MR.strings.action_bookmark
|
||||
},
|
||||
),
|
||||
icon = if (bookmarked) {
|
||||
Icons.Outlined.Bookmark
|
||||
} else {
|
||||
Icons.Outlined.BookmarkBorder
|
||||
},
|
||||
onClick = onToggleBookmarked,
|
||||
),
|
||||
)
|
||||
onOpenInWebView?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_open_in_web_view),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
onOpenInBrowser?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_open_in_browser),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
onShare?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
bookmarked = bookmarked,
|
||||
onToggleBookmarked = onToggleBookmarked,
|
||||
onOpenInWebView = onOpenInWebView,
|
||||
onOpenInBrowser = onOpenInBrowser,
|
||||
onShare = onShare,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,19 +97,12 @@ fun ReaderAppBars(
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = animationSpec,
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = animationSpec,
|
||||
),
|
||||
enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
|
||||
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
|
||||
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifierWithInsetsPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||
ChapterNavigator(
|
||||
isRtl = isRtl,
|
||||
onNextChapter = onNextChapter,
|
||||
@@ -178,8 +113,12 @@ fun ReaderAppBars(
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
)
|
||||
BottomReaderBar(
|
||||
backgroundColor = backgroundColor,
|
||||
ReaderBottomBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = MaterialTheme.padding.small)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
readingMode = readingMode,
|
||||
onClickReadingMode = onClickReadingMode,
|
||||
orientation = orientation,
|
||||
|
||||
+5
-11
@@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.reader.appbars
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -12,9 +9,8 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
@@ -22,8 +18,7 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun BottomReaderBar(
|
||||
backgroundColor: Color,
|
||||
fun ReaderBottomBar(
|
||||
readingMode: ReadingMode,
|
||||
onClickReadingMode: () -> Unit,
|
||||
orientation: ReaderOrientation,
|
||||
@@ -31,12 +26,11 @@ fun BottomReaderBar(
|
||||
cropEnabled: Boolean,
|
||||
onClickCropBorder: () -> Unit,
|
||||
onClickSettings: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor)
|
||||
.padding(8.dp),
|
||||
modifier = modifier
|
||||
.pointerInput(Unit) {},
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -0,0 +1,83 @@
|
||||
package eu.kanade.presentation.reader.appbars
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.BookmarkBorder
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun ReaderTopBar(
|
||||
mangaTitle: String?,
|
||||
chapterTitle: String?,
|
||||
navigateUp: () -> Unit,
|
||||
bookmarked: Boolean,
|
||||
onToggleBookmarked: () -> Unit,
|
||||
onOpenInWebView: (() -> Unit)?,
|
||||
onOpenInBrowser: (() -> Unit)?,
|
||||
onShare: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AppBar(
|
||||
modifier = modifier,
|
||||
backgroundColor = Color.Transparent,
|
||||
title = mangaTitle,
|
||||
subtitle = chapterTitle,
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(
|
||||
if (bookmarked) {
|
||||
MR.strings.action_remove_bookmark
|
||||
} else {
|
||||
MR.strings.action_bookmark
|
||||
},
|
||||
),
|
||||
icon = if (bookmarked) {
|
||||
Icons.Outlined.Bookmark
|
||||
} else {
|
||||
Icons.Outlined.BookmarkBorder
|
||||
},
|
||||
onClick = onToggleBookmarked,
|
||||
),
|
||||
)
|
||||
onOpenInWebView?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_open_in_web_view),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
onOpenInBrowser?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_open_in_browser),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
onShare?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -22,44 +22,44 @@ import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val customBrightness by screenModel.preferences.customBrightness().collectAsState()
|
||||
val customBrightness by screenModel.preferences.customBrightness.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
pref = screenModel.preferences.customBrightness(),
|
||||
pref = screenModel.preferences.customBrightness,
|
||||
)
|
||||
|
||||
/**
|
||||
/*
|
||||
* Sets the brightness of the screen. Range is [-75, 100].
|
||||
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
||||
* From 1 to 100 it sets that value as brightness.
|
||||
* 0 sets system brightness and hides the overlay.
|
||||
*/
|
||||
if (customBrightness) {
|
||||
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
|
||||
val customBrightnessValue by screenModel.preferences.customBrightnessValue.collectAsState()
|
||||
SliderItem(
|
||||
value = customBrightnessValue,
|
||||
valueRange = -75..100,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
|
||||
onChange = { screenModel.preferences.customBrightnessValue.set(it) },
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
val colorFilter by screenModel.preferences.colorFilter().collectAsState()
|
||||
val colorFilter by screenModel.preferences.colorFilter.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_custom_color_filter),
|
||||
pref = screenModel.preferences.colorFilter(),
|
||||
pref = screenModel.preferences.colorFilter,
|
||||
)
|
||||
if (colorFilter) {
|
||||
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
|
||||
val colorFilterValue by screenModel.preferences.colorFilterValue.collectAsState()
|
||||
SliderItem(
|
||||
value = colorFilterValue.red,
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
onChange = { newRValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newRValue, RED_MASK, 16)
|
||||
}
|
||||
},
|
||||
@@ -71,7 +71,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
onChange = { newGValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newGValue, GREEN_MASK, 8)
|
||||
}
|
||||
},
|
||||
@@ -83,7 +83,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
onChange = { newBValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newBValue, BLUE_MASK, 0)
|
||||
}
|
||||
},
|
||||
@@ -95,19 +95,19 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
onChange = { newAValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newAValue, ALPHA_MASK, 24)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_color_filter_mode) {
|
||||
ColorFilterMode.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = colorFilterMode == index,
|
||||
onClick = { screenModel.preferences.colorFilterMode().set(index) },
|
||||
onClick = { screenModel.preferences.colorFilterMode.set(index) },
|
||||
label = { Text(stringResource(it.first)) },
|
||||
)
|
||||
}
|
||||
@@ -116,11 +116,11 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_grayscale),
|
||||
pref = screenModel.preferences.grayscale(),
|
||||
pref = screenModel.preferences.grayscale,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_inverted_colors),
|
||||
pref = screenModel.preferences.invertedColors(),
|
||||
pref = screenModel.preferences.invertedColors,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -8,6 +9,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
@@ -31,24 +33,24 @@ private val flashColors = listOf(
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
|
||||
val readerTheme by screenModel.preferences.readerTheme.collectAsState()
|
||||
|
||||
val flashPageState by screenModel.preferences.flashOnPageChange().collectAsState()
|
||||
val flashPageState by screenModel.preferences.flashOnPageChange.collectAsState()
|
||||
|
||||
val flashMillisPref = screenModel.preferences.flashDurationMillis()
|
||||
val flashMillisPref = screenModel.preferences.flashDurationMillis
|
||||
val flashMillis by flashMillisPref.collectAsState()
|
||||
|
||||
val flashIntervalPref = screenModel.preferences.flashPageInterval()
|
||||
val flashIntervalPref = screenModel.preferences.flashPageInterval
|
||||
val flashInterval by flashIntervalPref.collectAsState()
|
||||
|
||||
val flashColorPref = screenModel.preferences.flashColor()
|
||||
val flashColorPref = screenModel.preferences.flashColor
|
||||
val flashColor by flashColorPref.collectAsState()
|
||||
|
||||
SettingsChipRow(MR.strings.pref_reader_theme) {
|
||||
themes.map { (labelRes, value) ->
|
||||
FilterChip(
|
||||
selected = readerTheme == value,
|
||||
onClick = { screenModel.preferences.readerTheme().set(value) },
|
||||
onClick = { screenModel.preferences.readerTheme.set(value) },
|
||||
label = { Text(stringResource(labelRes)) },
|
||||
)
|
||||
}
|
||||
@@ -56,51 +58,52 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_show_page_number),
|
||||
pref = screenModel.preferences.showPageNumber(),
|
||||
pref = screenModel.preferences.showPageNumber,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_fullscreen),
|
||||
pref = screenModel.preferences.fullscreen(),
|
||||
pref = screenModel.preferences.fullscreen,
|
||||
)
|
||||
|
||||
if (screenModel.hasDisplayCutout && screenModel.preferences.fullscreen().get()) {
|
||||
val isFullscreen by screenModel.preferences.fullscreen.collectAsState()
|
||||
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_cutout_short),
|
||||
pref = screenModel.preferences.cutoutShort(),
|
||||
pref = screenModel.preferences.drawUnderCutout,
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_keep_screen_on),
|
||||
pref = screenModel.preferences.keepScreenOn(),
|
||||
pref = screenModel.preferences.keepScreenOn,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
pref = screenModel.preferences.readWithLongTap(),
|
||||
pref = screenModel.preferences.readWithLongTap,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
pref = screenModel.preferences.alwaysShowChapterTransition(),
|
||||
pref = screenModel.preferences.alwaysShowChapterTransition,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_transitions),
|
||||
pref = screenModel.preferences.pageTransitions(),
|
||||
pref = screenModel.preferences.pageTransitions,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_flash_page),
|
||||
pref = screenModel.preferences.flashOnPageChange(),
|
||||
pref = screenModel.preferences.flashOnPageChange,
|
||||
)
|
||||
if (flashPageState) {
|
||||
SliderItem(
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
valueRange = 1..15,
|
||||
label = stringResource(MR.strings.pref_flash_duration),
|
||||
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
valueString = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
@@ -108,7 +111,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
value = flashInterval,
|
||||
valueRange = 1..10,
|
||||
label = stringResource(MR.strings.pref_flash_page_interval),
|
||||
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
valueString = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
onChange = {
|
||||
flashIntervalPref.set(it)
|
||||
},
|
||||
|
||||
@@ -63,32 +63,32 @@ internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel)
|
||||
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
|
||||
HeadingItem(MR.strings.pager_viewer)
|
||||
|
||||
val navigationModePager by screenModel.preferences.navigationModePager().collectAsState()
|
||||
val pagerNavInverted by screenModel.preferences.pagerNavInverted().collectAsState()
|
||||
val navigationModePager by screenModel.preferences.navigationModePager.collectAsState()
|
||||
val pagerNavInverted by screenModel.preferences.pagerNavInverted.collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModePager,
|
||||
onSelect = screenModel.preferences.navigationModePager()::set,
|
||||
onSelect = screenModel.preferences.navigationModePager::set,
|
||||
invertMode = pagerNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.pagerNavInverted()::set,
|
||||
onSelectInvertMode = screenModel.preferences.pagerNavInverted::set,
|
||||
)
|
||||
|
||||
val imageScaleType by screenModel.preferences.imageScaleType().collectAsState()
|
||||
val imageScaleType by screenModel.preferences.imageScaleType.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_image_scale_type) {
|
||||
ReaderPreferences.ImageScaleType.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = imageScaleType == index + 1,
|
||||
onClick = { screenModel.preferences.imageScaleType().set(index + 1) },
|
||||
onClick = { screenModel.preferences.imageScaleType.set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val zoomStart by screenModel.preferences.zoomStart().collectAsState()
|
||||
val zoomStart by screenModel.preferences.zoomStart.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_zoom_start) {
|
||||
ReaderPreferences.ZoomStart.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = zoomStart == index + 1,
|
||||
onClick = { screenModel.preferences.zoomStart().set(index + 1) },
|
||||
onClick = { screenModel.preferences.zoomStart.set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
@@ -96,42 +96,42 @@ private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenMod
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_crop_borders),
|
||||
pref = screenModel.preferences.cropBorders(),
|
||||
pref = screenModel.preferences.cropBorders,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_landscape_zoom),
|
||||
pref = screenModel.preferences.landscapeZoom(),
|
||||
pref = screenModel.preferences.landscapeZoom,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_navigate_pan),
|
||||
pref = screenModel.preferences.navigateToPan(),
|
||||
pref = screenModel.preferences.navigateToPan,
|
||||
)
|
||||
|
||||
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged().collectAsState()
|
||||
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_split),
|
||||
pref = screenModel.preferences.dualPageSplitPaged(),
|
||||
pref = screenModel.preferences.dualPageSplitPaged,
|
||||
)
|
||||
|
||||
if (dualPageSplitPaged) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_invert),
|
||||
pref = screenModel.preferences.dualPageInvertPaged(),
|
||||
pref = screenModel.preferences.dualPageInvertPaged,
|
||||
)
|
||||
}
|
||||
|
||||
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit().collectAsState()
|
||||
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate),
|
||||
pref = screenModel.preferences.dualPageRotateToFit(),
|
||||
pref = screenModel.preferences.dualPageRotateToFit,
|
||||
)
|
||||
|
||||
if (dualPageRotateToFit) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvert(),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvert,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -142,65 +142,65 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
|
||||
HeadingItem(MR.strings.webtoon_viewer)
|
||||
|
||||
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon().collectAsState()
|
||||
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted().collectAsState()
|
||||
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon.collectAsState()
|
||||
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted.collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModeWebtoon,
|
||||
onSelect = screenModel.preferences.navigationModeWebtoon()::set,
|
||||
onSelect = screenModel.preferences.navigationModeWebtoon::set,
|
||||
invertMode = webtoonNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.webtoonNavInverted()::set,
|
||||
onSelectInvertMode = screenModel.preferences.webtoonNavInverted::set,
|
||||
)
|
||||
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding.collectAsState()
|
||||
SliderItem(
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
valueText = numberFormat.format(webtoonSidePadding / 100f),
|
||||
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onChange = {
|
||||
screenModel.preferences.webtoonSidePadding().set(it)
|
||||
screenModel.preferences.webtoonSidePadding.set(it)
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_crop_borders),
|
||||
pref = screenModel.preferences.cropBordersWebtoon(),
|
||||
pref = screenModel.preferences.cropBordersWebtoon,
|
||||
)
|
||||
|
||||
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState()
|
||||
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_split),
|
||||
pref = screenModel.preferences.dualPageSplitWebtoon(),
|
||||
pref = screenModel.preferences.dualPageSplitWebtoon,
|
||||
)
|
||||
|
||||
if (dualPageSplitWebtoon) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_invert),
|
||||
pref = screenModel.preferences.dualPageInvertWebtoon(),
|
||||
pref = screenModel.preferences.dualPageInvertWebtoon,
|
||||
)
|
||||
}
|
||||
|
||||
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon().collectAsState()
|
||||
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate),
|
||||
pref = screenModel.preferences.dualPageRotateToFitWebtoon(),
|
||||
pref = screenModel.preferences.dualPageRotateToFitWebtoon,
|
||||
)
|
||||
|
||||
if (dualPageRotateToFitWebtoon) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon(),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon,
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut(),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.presentation.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
@@ -34,8 +35,8 @@ fun TachiyomiTheme(
|
||||
) {
|
||||
val uiPreferences = Injekt.get<UiPreferences>()
|
||||
BaseTachiyomiTheme(
|
||||
appTheme = appTheme ?: uiPreferences.appTheme().get(),
|
||||
isAmoled = amoled ?: uiPreferences.themeDarkAmoled().get(),
|
||||
appTheme = appTheme ?: uiPreferences.appTheme.get(),
|
||||
isAmoled = amoled ?: uiPreferences.themeDarkAmoled.get(),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
||||
isAmoled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||
getThemeColorScheme(
|
||||
context = context,
|
||||
appTheme = appTheme,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getThemeColorScheme(
|
||||
context: Context,
|
||||
appTheme: AppTheme,
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
MonetColorScheme(context)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
||||
private val surfaceContainerHigh = Color(0xFF131313)
|
||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
fun getColorScheme(
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
overrideDarkSurfaceContainers: Boolean,
|
||||
): ColorScheme {
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
val amoledScheme = darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||
|
||||
return amoledScheme.copy(
|
||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||
surfaceContainerLowest = surfaceContainer,
|
||||
surfaceContainerLow = surfaceContainer,
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.UiModeManager
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
||||
import com.google.android.material.color.utilities.SchemeContent
|
||||
import com.google.android.material.color.utilities.Score
|
||||
import com.materialkolor.PaletteStyle
|
||||
import com.materialkolor.dynamiccolor.ColorSpec
|
||||
import com.materialkolor.ktx.DynamicScheme
|
||||
import com.materialkolor.toColorScheme
|
||||
|
||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
?.primaryColor
|
||||
?.toArgb()
|
||||
if (seed != null) {
|
||||
MonetCompatColorScheme(context, seed)
|
||||
MonetCompatColorScheme(Color(seed))
|
||||
} else {
|
||||
TachiyomiColorScheme
|
||||
}
|
||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme
|
||||
get() = monet.lightScheme
|
||||
|
||||
companion object {
|
||||
@Suppress("Unused")
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val bitmapPixels = IntArray(width * height)
|
||||
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
|
||||
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
|
||||
.takeIf { it != 0 } // Don't take fallback color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
||||
override val darkScheme = dynamicDarkColorScheme(context)
|
||||
}
|
||||
|
||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
||||
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||
|
||||
companion object {
|
||||
private fun Int.toComposeColor(): Color = Color(this)
|
||||
|
||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
||||
val scheme = SchemeContent(
|
||||
Hct.fromInt(seed),
|
||||
dark,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
)
|
||||
val dynamicColors = MaterialDynamicColors()
|
||||
return ColorScheme(
|
||||
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
|
||||
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
|
||||
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
|
||||
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
|
||||
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
|
||||
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
|
||||
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
|
||||
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
|
||||
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
|
||||
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
|
||||
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
|
||||
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
|
||||
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
|
||||
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
|
||||
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
|
||||
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
|
||||
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
|
||||
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
|
||||
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
|
||||
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
|
||||
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
|
||||
scrim = Color.Black,
|
||||
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
|
||||
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
|
||||
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||
return DynamicScheme(
|
||||
seedColor = seed,
|
||||
isDark = dark,
|
||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||
style = PaletteStyle.Expressive,
|
||||
)
|
||||
.toColorScheme(isAmoled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.platform.toClipEntry
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
@@ -73,6 +75,7 @@ import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
@@ -248,6 +251,7 @@ private fun SearchResultItem(
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
||||
expanded = dropDownMenuExpanded,
|
||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||
onCopyName = {
|
||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
||||
scope.launch {
|
||||
val clipEntry = ClipData.newPlainText(
|
||||
trackSearch.title,
|
||||
trackSearch.title,
|
||||
).toClipEntry()
|
||||
clipboard.setClipEntry(clipEntry)
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = {
|
||||
val url = trackSearch.tracking_url
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -30,18 +26,13 @@ fun TrackLogoIcon(
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
)
|
||||
}
|
||||
.clip(MaterialTheme.shapes.medium),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
|
||||
-4
@@ -1,8 +1,6 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.test.DummyTracker
|
||||
|
||||
@@ -13,8 +11,6 @@ internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker>
|
||||
DummyTracker(
|
||||
id = 1L,
|
||||
name = "Dummy Tracker",
|
||||
valLogoColor = Color.rgb(18, 25, 35),
|
||||
valLogo = R.drawable.ic_tracker_anilist,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun UpdatesFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(screenModel = screenModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterSheet(
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = filterDownloaded,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
)
|
||||
|
||||
val filterUnread by screenModel.updatesPreferences.filterUnread.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
)
|
||||
|
||||
val filterStarted by screenModel.updatesPreferences.filterStarted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
)
|
||||
|
||||
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
|
||||
|
||||
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators.collectAsState()
|
||||
|
||||
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { toggleScanlatorFilter() }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_filter_excluded_scanlators),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = filterExcludedScanlators,
|
||||
onCheckedChange = { toggleScanlatorFilter() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -37,6 +40,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -54,8 +58,10 @@ fun UpdateScreen(
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
onSelectAll(false)
|
||||
@@ -66,6 +72,8 @@ fun UpdateScreen(
|
||||
UpdatesAppBar(
|
||||
onCalendarClicked = { onCalendarClicked() },
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
onFilterClicked = { onFilterClicked() },
|
||||
hasFilters = hasActiveFilters,
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
@@ -133,6 +141,8 @@ fun UpdateScreen(
|
||||
private fun UpdatesAppBar(
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasFilters: Boolean,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
@@ -147,6 +157,12 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onFilterClicked,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_view_upcoming),
|
||||
icon = Icons.Outlined.CalendarMonth,
|
||||
|
||||
@@ -69,7 +69,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
|
||||
internal fun LazyListScope.updatesUiItems(
|
||||
uiModels: List<UpdatesUiModel>,
|
||||
selectionMode: Boolean,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
@@ -111,11 +111,11 @@ internal fun LazyListScope.updatesUiItems(
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||
},
|
||||
onClick = {
|
||||
when {
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||
else -> onClickUpdate(updatesItem)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
import tachiyomi.i18n.MR
|
||||
import java.net.UnknownHostException
|
||||
|
||||
context(Context)
|
||||
context(context: Context)
|
||||
val Throwable.formattedMessage: String
|
||||
get() {
|
||||
when (this) {
|
||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
||||
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||
is UnknownHostException -> {
|
||||
return if (!isOnline()) {
|
||||
stringResource(MR.strings.exception_offline)
|
||||
return if (!context.isOnline()) {
|
||||
context.stringResource(MR.strings.exception_offline)
|
||||
} else {
|
||||
stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
context.stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
||||
is NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||
}
|
||||
return when (val className = this::class.simpleName) {
|
||||
"Exception", "IOException" -> message ?: className
|
||||
|
||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
// https://issuetracker.google.com/352584409
|
||||
context(LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
context(itemScope: LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ package eu.kanade.presentation.webview
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Message
|
||||
import android.webkit.JsPromptResult
|
||||
import android.webkit.JsResult
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -18,7 +22,9 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -26,17 +32,23 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
||||
import com.kevinnzou.web.AccompanistWebChromeClient
|
||||
import com.kevinnzou.web.AccompanistWebViewClient
|
||||
import com.kevinnzou.web.LoadingState
|
||||
import com.kevinnzou.web.WebContent
|
||||
import com.kevinnzou.web.WebView
|
||||
import com.kevinnzou.web.rememberWebViewNavigator
|
||||
import com.kevinnzou.web.rememberWebViewState
|
||||
import com.kevinnzou.web.WebViewNavigator
|
||||
import com.kevinnzou.web.WebViewState
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getHtml
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -44,6 +56,18 @@ import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
|
||||
var state by mutableStateOf(WebViewState(webContent))
|
||||
var popupMessage: Message? = null
|
||||
private set
|
||||
var webView: WebView? = null
|
||||
|
||||
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
|
||||
this.popupMessage = popupMessage
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WebViewScreenContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
@@ -55,13 +79,30 @@ fun WebViewScreenContent(
|
||||
headers: Map<String, String> = emptyMap(),
|
||||
onUrlChange: (String) -> Unit = {},
|
||||
) {
|
||||
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
||||
val navigator = rememberWebViewNavigator()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val windowStack = remember {
|
||||
mutableStateStackOf(
|
||||
WebViewWindow(
|
||||
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
||||
WebViewNavigator(coroutineScope),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val currentWindow = windowStack.lastItemOrNull!!
|
||||
val navigator = currentWindow.navigator
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var currentUrl by remember { mutableStateOf(url) }
|
||||
var showCloudflareHelp by remember { mutableStateOf(false) }
|
||||
var isActive by remember { mutableStateOf(true) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { isActive = false }
|
||||
}
|
||||
|
||||
val webClient = remember {
|
||||
object : AccompanistWebViewClient() {
|
||||
@@ -97,31 +138,97 @@ fun WebViewScreenContent(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
request?.let {
|
||||
// Don't attempt to open blobs as webpages
|
||||
if (it.url.toString().startsWith("blob:http")) {
|
||||
return false
|
||||
}
|
||||
val url = request?.url?.toString() ?: return false
|
||||
|
||||
// Ignore intents urls
|
||||
if (it.url.toString().startsWith("intent://")) {
|
||||
// Ignore intents urls
|
||||
if (url.startsWith("intent://")) return true
|
||||
|
||||
// Only open valid web urls
|
||||
if (url.startsWith("http") || url.startsWith("https")) {
|
||||
if (url != view?.url) {
|
||||
view?.loadUrl(url, headers)
|
||||
return true
|
||||
}
|
||||
|
||||
// Continue with request, but with custom headers
|
||||
view?.loadUrl(it.url.toString(), headers)
|
||||
}
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val webChromeClient = remember {
|
||||
object : AccompanistWebChromeClient() {
|
||||
override fun onCreateWindow(
|
||||
view: WebView,
|
||||
isDialog: Boolean,
|
||||
isUserGesture: Boolean,
|
||||
resultMsg: Message,
|
||||
): Boolean {
|
||||
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
||||
if (isUserGesture) {
|
||||
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onJsAlert(view: WebView, url: String?, message: String?, result: JsResult): Boolean {
|
||||
if (!isActive) {
|
||||
result.confirm()
|
||||
return true
|
||||
}
|
||||
return super.onJsAlert(view, url, message, result)
|
||||
}
|
||||
|
||||
override fun onJsConfirm(view: WebView, url: String?, message: String?, result: JsResult): Boolean {
|
||||
if (!isActive) {
|
||||
result.cancel()
|
||||
return true
|
||||
}
|
||||
return super.onJsConfirm(view, url, message, result)
|
||||
}
|
||||
|
||||
override fun onJsPrompt(
|
||||
view: WebView,
|
||||
url: String?,
|
||||
message: String?,
|
||||
defaultValue: String?,
|
||||
result: JsPromptResult,
|
||||
): Boolean {
|
||||
if (!isActive) {
|
||||
result.cancel()
|
||||
return true
|
||||
}
|
||||
return super.onJsPrompt(view, url, message, defaultValue, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initializePopup(webView: WebView, message: Message): WebView {
|
||||
val transport = message.obj as WebView.WebViewTransport
|
||||
transport.webView = webView
|
||||
message.sendToTarget()
|
||||
return webView
|
||||
}
|
||||
|
||||
val popState = remember<() -> Unit> {
|
||||
{
|
||||
if (windowStack.size == 1) {
|
||||
onNavigateUp()
|
||||
} else {
|
||||
windowStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(windowStack.size > 1, popState)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Box {
|
||||
Column {
|
||||
AppBar(
|
||||
title = state.pageTitle ?: initialTitle,
|
||||
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||
subtitle = currentUrl,
|
||||
navigateUp = onNavigateUp,
|
||||
navigationIcon = Icons.Outlined.Close,
|
||||
@@ -164,7 +271,18 @@ fun WebViewScreenContent(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = { onClearCookies(currentUrl) },
|
||||
),
|
||||
),
|
||||
).builder().apply {
|
||||
if (windowStack.size > 1) {
|
||||
add(
|
||||
0,
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_webview_close_tab),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
|
||||
onClick = popState,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.build(),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -186,14 +304,14 @@ fun WebViewScreenContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
when (val loadingState = state.loadingState) {
|
||||
when (val loadingState = currentWindow.state.loadingState) {
|
||||
is LoadingState.Initializing -> LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
is LoadingState.Loading -> LinearProgressIndicator(
|
||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
||||
progress = { loadingState.progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
@@ -203,27 +321,55 @@ fun WebViewScreenContent(
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
WebView(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding),
|
||||
navigator = navigator,
|
||||
onCreated = { webView ->
|
||||
webView.setDefaultSettings()
|
||||
// We need to key the WebView composable to the window object since simply updating the WebView composable will
|
||||
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
|
||||
// completely reset the WebView composable when the current window switches.
|
||||
key(currentWindow) {
|
||||
WebView(
|
||||
state = currentWindow.state,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding),
|
||||
navigator = navigator,
|
||||
onCreated = { webView ->
|
||||
webView.setDefaultSettings()
|
||||
|
||||
// Debug mode (chrome://inspect/#devices)
|
||||
if (BuildConfig.DEBUG &&
|
||||
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||
) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
// Debug mode (chrome://inspect/#devices)
|
||||
if (BuildConfig.DEBUG &&
|
||||
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||
) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
|
||||
headers["user-agent"]?.let {
|
||||
webView.settings.userAgentString = it
|
||||
}
|
||||
},
|
||||
client = webClient,
|
||||
)
|
||||
headers["user-agent"]?.let {
|
||||
webView.settings.userAgentString = it
|
||||
}
|
||||
},
|
||||
onDispose = { webView ->
|
||||
val window = windowStack.items.find { it.webView == webView }
|
||||
if (window == null) {
|
||||
// If we couldn't find any window on the stack that owns this WebView, it means that we can
|
||||
// safely dispose of it because the window containing it has been closed.
|
||||
webView.destroy()
|
||||
} else {
|
||||
// The composable is being disposed but the WebView object is not.
|
||||
// When the WebView element is recomposed, we will want the WebView to resume from its state
|
||||
// before it was unmounted, we won't want it to reset back to its original target.
|
||||
window.state.content = WebContent.NavigatorOnly
|
||||
}
|
||||
},
|
||||
client = webClient,
|
||||
chromeClient = webChromeClient,
|
||||
factory = { context ->
|
||||
currentWindow.webView
|
||||
?: WebView(context).also { webView ->
|
||||
currentWindow.webView = webView
|
||||
currentWindow.popupMessage?.let {
|
||||
initializePopup(webView, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
@@ -106,7 +107,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
// Show notification to disable Incognito Mode when it's enabled
|
||||
basePreferences.incognitoMode().changes()
|
||||
basePreferences.incognitoMode.changes()
|
||||
.onEach { enabled ->
|
||||
if (enabled) {
|
||||
disableIncognitoReceiver.register()
|
||||
@@ -122,7 +123,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this@App,
|
||||
0,
|
||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
setContentIntent(pendingIntent)
|
||||
@@ -134,31 +135,37 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.analytics()
|
||||
privacyPreferences.analytics
|
||||
.changes()
|
||||
.onEach(TelemetryConfig::setAnalyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.crashlytics()
|
||||
privacyPreferences.crashlytics
|
||||
.changes()
|
||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||
basePreferences.hardwareBitmapThreshold.let { preference ->
|
||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||
}
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().changes()
|
||||
basePreferences.hardwareBitmapThreshold.changes()
|
||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||
.launchIn(scope)
|
||||
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode.get())
|
||||
|
||||
// Updates widget update
|
||||
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
||||
|
||||
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
val minLogPriority = when {
|
||||
networkPreferences.verboseLogging.get() -> LogPriority.VERBOSE
|
||||
BuildConfig.DEBUG -> LogPriority.DEBUG
|
||||
else -> LogPriority.INFO
|
||||
}
|
||||
LogcatLogger.install()
|
||||
LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority)
|
||||
}
|
||||
|
||||
initializeMigrator()
|
||||
@@ -196,9 +203,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
add(MangaKeyer())
|
||||
}
|
||||
|
||||
memoryCache(
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context)
|
||||
.build(),
|
||||
)
|
||||
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
if (networkPreferences.verboseLogging.get()) logger(DebugLogger())
|
||||
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(8))
|
||||
@@ -220,8 +233,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val isChromiumCall = stackTrace.any { trace ->
|
||||
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
|
||||
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
|
||||
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
||||
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
||||
}
|
||||
|
||||
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
||||
@@ -243,7 +256,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
private var registered = false
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
basePreferences.incognitoMode().set(false)
|
||||
basePreferences.incognitoMode.set(false)
|
||||
}
|
||||
|
||||
fun register() {
|
||||
|
||||
@@ -101,7 +101,7 @@ class BackupNotifier(private val context: Context) {
|
||||
}
|
||||
setContentTitle(contentTitle)
|
||||
|
||||
if (!preferences.hideNotificationContent().get()) {
|
||||
if (!preferences.hideNotificationContent.get()) {
|
||||
setContentText(content)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||
|
||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val interval = prefInterval ?: backupPreferences.backupInterval().get()
|
||||
val interval = prefInterval ?: backupPreferences.backupInterval.get()
|
||||
if (interval > 0) {
|
||||
val constraints = Constraints(
|
||||
requiresBatteryNotLow = true,
|
||||
|
||||
@@ -108,7 +108,7 @@ class BackupCreator(
|
||||
BackupFileValidator(context).validate(fileUri)
|
||||
|
||||
if (isAutoBackup) {
|
||||
backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
|
||||
backupPreferences.lastAutoBackupTimestamp.set(Instant.now().toEpochMilli())
|
||||
}
|
||||
|
||||
return fileUri.toString()
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class CategoriesRestorer(
|
||||
.let { id -> it.toCategory(id).copy(order = order) }
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
libraryPreferences.categorizedDisplaySettings.set(
|
||||
(dbCategories + categories)
|
||||
.distinctBy { it.flags }
|
||||
.size > 1,
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.jakewharton.disklrucache.DiskLruCache
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import okhttp3.Response
|
||||
@@ -115,7 +114,7 @@ class ChapterCache(
|
||||
fun isImageInCache(imageUrl: String): Boolean {
|
||||
return try {
|
||||
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -147,7 +146,7 @@ class ChapterCache(
|
||||
try {
|
||||
// Get editor from md5 key.
|
||||
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||
editor = diskCache.edit(key) ?: return
|
||||
|
||||
// Get OutputStream and write image with Okio.
|
||||
response.body.source().saveTo(editor.newOutputStream(0))
|
||||
|
||||
@@ -128,6 +128,7 @@ class DownloadCache(
|
||||
*
|
||||
* @param chapterName the name of the chapter to query.
|
||||
* @param chapterScanlator scanlator of the chapter to query
|
||||
* @param chapterUrl the url of the chapter to query
|
||||
* @param mangaTitle the title of the manga to query.
|
||||
* @param sourceId the id of the source of the chapter.
|
||||
* @param skipCache whether to skip the directory cache and check in the filesystem.
|
||||
@@ -135,13 +136,14 @@ class DownloadCache(
|
||||
fun isChapterDownloaded(
|
||||
chapterName: String,
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
mangaTitle: String,
|
||||
sourceId: Long,
|
||||
skipCache: Boolean,
|
||||
): Boolean {
|
||||
if (skipCache) {
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
|
||||
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
|
||||
}
|
||||
|
||||
renewCache()
|
||||
@@ -153,6 +155,7 @@ class DownloadCache(
|
||||
return provider.getValidChapterDirNames(
|
||||
chapterName,
|
||||
chapterScanlator,
|
||||
chapterUrl,
|
||||
).any { it in mangaDir.chapterDirs }
|
||||
}
|
||||
}
|
||||
@@ -233,7 +236,7 @@ class DownloadCache(
|
||||
rootDownloadsDirMutex.withLock {
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||
if (it in mangaDir.chapterDirs) {
|
||||
mangaDir.chapterDirs -= it
|
||||
}
|
||||
@@ -254,7 +257,7 @@ class DownloadCache(
|
||||
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
|
||||
chapters.forEach { chapter ->
|
||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
|
||||
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
|
||||
if (it in mangaDir.chapterDirs) {
|
||||
mangaDir.chapterDirs -= it
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
|
||||
override suspend fun doWork(): Result {
|
||||
var networkCheck = checkNetworkState(
|
||||
applicationContext.activeNetworkState(),
|
||||
downloadPreferences.downloadOnlyOverWifi().get(),
|
||||
downloadPreferences.downloadOnlyOverWifi.get(),
|
||||
)
|
||||
var active = networkCheck && downloadManager.downloaderStart()
|
||||
|
||||
@@ -69,7 +69,7 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
|
||||
coroutineScope {
|
||||
combineTransform(
|
||||
applicationContext.networkStateFlow(),
|
||||
downloadPreferences.downloadOnlyOverWifi().changes(),
|
||||
downloadPreferences.downloadOnlyOverWifi.changes(),
|
||||
transform = { a, b -> emit(checkNetworkState(a, b)) },
|
||||
)
|
||||
.onEach { networkCheck = it }
|
||||
|
||||
@@ -159,7 +159,7 @@ class DownloadManager(
|
||||
* @return the list of pages from the chapter.
|
||||
*/
|
||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
||||
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
|
||||
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, chapter.url, manga.title, source)
|
||||
val files = chapterDir?.listFiles().orEmpty()
|
||||
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||
|
||||
@@ -185,11 +185,12 @@ class DownloadManager(
|
||||
fun isChapterDownloaded(
|
||||
chapterName: String,
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
mangaTitle: String,
|
||||
sourceId: Long,
|
||||
skipCache: Boolean = false,
|
||||
): Boolean {
|
||||
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
|
||||
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,7 +369,7 @@ class DownloadManager(
|
||||
* @param newChapter the target chapter with the new name.
|
||||
*/
|
||||
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
|
||||
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator, oldChapter.url)
|
||||
val mangaDir = provider.getMangaDir(manga.title, source).getOrElse { e ->
|
||||
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
|
||||
return
|
||||
@@ -379,7 +380,7 @@ class DownloadManager(
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull() ?: return
|
||||
|
||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
|
||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
||||
newName += ".cbz"
|
||||
}
|
||||
@@ -396,7 +397,7 @@ class DownloadManager(
|
||||
|
||||
private suspend fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong)
|
||||
val categoriesToExclude = downloadPreferences.removeExcludeCategories.get().map(String::toLong)
|
||||
|
||||
val categoriesForManga = getCategories.await(manga.id)
|
||||
.map { it.id }
|
||||
@@ -407,7 +408,7 @@ class DownloadManager(
|
||||
chapters
|
||||
}
|
||||
|
||||
return if (!downloadPreferences.removeBookmarkedChapters().get()) {
|
||||
return if (!downloadPreferences.removeBookmarkedChapters.get()) {
|
||||
filteredCategoryManga.filterNot { it.bookmark }
|
||||
} else {
|
||||
filteredCategoryManga
|
||||
|
||||
@@ -96,7 +96,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
download.pages!!.size,
|
||||
)
|
||||
|
||||
if (preferences.hideNotificationContent().get()) {
|
||||
if (preferences.hideNotificationContent.get()) {
|
||||
setContentTitle(downloadingProgressText)
|
||||
setContentText(null)
|
||||
} else {
|
||||
|
||||
@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
|
||||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.lang.Hash.md5
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.storage.service.StorageManager
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -25,6 +27,7 @@ import java.io.IOException
|
||||
class DownloadProvider(
|
||||
private val context: Context,
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val downloadsDir: UniFile?
|
||||
@@ -96,9 +99,15 @@ class DownloadProvider(
|
||||
* @param mangaTitle the title of the manga to query.
|
||||
* @param source the source of the chapter.
|
||||
*/
|
||||
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
|
||||
fun findChapterDir(
|
||||
chapterName: String,
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
mangaTitle: String,
|
||||
source: Source,
|
||||
): UniFile? {
|
||||
val mangaDir = findMangaDir(mangaTitle, source)
|
||||
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
|
||||
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
@@ -113,7 +122,7 @@ class DownloadProvider(
|
||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
|
||||
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
||||
return mangaDir to chapters.mapNotNull { chapter ->
|
||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
||||
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
@@ -125,7 +134,10 @@ class DownloadProvider(
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun getSourceDirName(source: Source): String {
|
||||
return DiskUtil.buildValidFilename(source.toString())
|
||||
return DiskUtil.buildValidFilename(
|
||||
source.toString(),
|
||||
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames.get(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,23 +146,75 @@ class DownloadProvider(
|
||||
* @param mangaTitle the title of the manga to query.
|
||||
*/
|
||||
fun getMangaDirName(mangaTitle: String): String {
|
||||
return DiskUtil.buildValidFilename(mangaTitle)
|
||||
return DiskUtil.buildValidFilename(
|
||||
mangaTitle,
|
||||
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames.get(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chapter directory name for a chapter.
|
||||
*
|
||||
* @param chapterName the name of the chapter to query.
|
||||
* @param chapterScanlator scanlator of the chapter to query
|
||||
* @param chapterScanlator scanlator of the chapter to query.
|
||||
* @param chapterUrl url of the chapter to query.
|
||||
*/
|
||||
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
|
||||
val newChapterName = sanitizeChapterName(chapterName)
|
||||
return DiskUtil.buildValidFilename(
|
||||
fun getChapterDirName(
|
||||
chapterName: String,
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames.get(),
|
||||
): String {
|
||||
var dirName = sanitizeChapterName(chapterName)
|
||||
if (!chapterScanlator.isNullOrBlank()) {
|
||||
dirName = chapterScanlator + "_" + dirName
|
||||
}
|
||||
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
|
||||
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
|
||||
dirName += "_" + md5(chapterUrl).take(6)
|
||||
return dirName
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of names that might have been previously used as
|
||||
* the directory name for a chapter.
|
||||
* Add to this list if naming pattern ever changes.
|
||||
*
|
||||
* @param chapterName the name of the chapter to query.
|
||||
* @param chapterScanlator scanlator of the chapter to query.
|
||||
* @param chapterUrl url of the chapter to query.
|
||||
*/
|
||||
private fun getLegacyChapterDirNames(
|
||||
chapterName: String,
|
||||
chapterScanlator: String?,
|
||||
chapterUrl: String,
|
||||
): List<String> {
|
||||
val sanitizedChapterName = sanitizeChapterName(chapterName)
|
||||
val chapterNameV1 = DiskUtil.buildValidFilename(
|
||||
when {
|
||||
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
|
||||
else -> newChapterName
|
||||
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
|
||||
else -> sanitizedChapterName
|
||||
},
|
||||
)
|
||||
|
||||
// Get the filename that would be generated if the user were
|
||||
// using the other value for the disallow non-ASCII
|
||||
// filenames setting. This ensures that chapters downloaded
|
||||
// before the user changed the setting can still be found.
|
||||
val otherChapterDirName =
|
||||
getChapterDirName(
|
||||
chapterName,
|
||||
chapterScanlator,
|
||||
chapterUrl,
|
||||
!libraryPreferences.disallowNonAsciiFilenames.get(),
|
||||
)
|
||||
|
||||
return buildList(2) {
|
||||
// Chapter name without hash (unable to handle duplicate
|
||||
// chapter names)
|
||||
add(chapterNameV1)
|
||||
add(otherChapterDirName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,24 +229,30 @@ class DownloadProvider(
|
||||
}
|
||||
|
||||
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
|
||||
return oldChapter.name != newChapter.name ||
|
||||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
|
||||
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
|
||||
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid downloaded chapter directory names.
|
||||
*
|
||||
* @param chapterName the name of the chapter to query.
|
||||
* @param chapterScanlator scanlator of the chapter to query
|
||||
* @param chapter the domain chapter object.
|
||||
*/
|
||||
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
|
||||
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
|
||||
return buildList(2) {
|
||||
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
|
||||
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
|
||||
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
|
||||
|
||||
return buildList {
|
||||
// Folder of images
|
||||
add(chapterDirName)
|
||||
|
||||
// Archived chapters
|
||||
add("$chapterDirName.cbz")
|
||||
|
||||
// any legacy names
|
||||
legacyChapterDirNames.forEach {
|
||||
add(it)
|
||||
add("$it.cbz")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,15 +191,17 @@ class Downloader(
|
||||
if (isRunning) return
|
||||
|
||||
downloaderJob = scope.launch {
|
||||
val activeDownloadsFlow = queueState.transformLatest { queue ->
|
||||
val activeDownloadsFlow = combine(
|
||||
queueState,
|
||||
downloadPreferences.parallelSourceLimit.changes(),
|
||||
) { a, b -> a to b }.transformLatest { (queue, parallelCount) ->
|
||||
while (true) {
|
||||
val activeDownloads = queue.asSequence()
|
||||
// Ignore completed downloads, leave them in the queue
|
||||
.filter { it.status.value <= Download.State.DOWNLOADING.value }
|
||||
.groupBy { it.source }
|
||||
.toList()
|
||||
// Concurrently download from 5 different sources
|
||||
.take(5)
|
||||
.take(parallelCount)
|
||||
.map { (_, downloads) -> downloads.first() }
|
||||
emit(activeDownloads)
|
||||
|
||||
@@ -211,7 +213,8 @@ class Downloader(
|
||||
}.filter { it }
|
||||
activeDownloadsErroredFlow.first()
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
|
||||
supervisorScope {
|
||||
@@ -274,7 +277,7 @@ class Downloader(
|
||||
val wasEmpty = queueState.value.isEmpty()
|
||||
val chaptersToQueue = chapters.asSequence()
|
||||
// Filter out those already downloaded.
|
||||
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
|
||||
.filter { provider.findChapterDir(it.name, it.scanlator, it.url, manga.title, source) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
// Filter out those already enqueued.
|
||||
@@ -299,7 +302,10 @@ class Downloader(
|
||||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
|
||||
) {
|
||||
notifier.onWarning(
|
||||
context.stringResource(MR.strings.download_queue_size_warning),
|
||||
context.stringResource(
|
||||
MR.strings.download_queue_size_warning,
|
||||
context.stringResource(MR.strings.app_name),
|
||||
),
|
||||
WARNING_NOTIF_TIMEOUT_MS,
|
||||
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
|
||||
)
|
||||
@@ -333,7 +339,11 @@ class Downloader(
|
||||
return
|
||||
}
|
||||
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
|
||||
val chapterDirname = provider.getChapterDirName(
|
||||
download.chapter.name,
|
||||
download.chapter.scanlator,
|
||||
download.chapter.url,
|
||||
)
|
||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||
|
||||
try {
|
||||
@@ -359,24 +369,23 @@ class Downloader(
|
||||
download.status = Download.State.DOWNLOADING
|
||||
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
// Concurrently do 2 pages at a time
|
||||
pageList.asFlow()
|
||||
.flatMapMerge(concurrency = 2) { page ->
|
||||
flow {
|
||||
// Fetch image URL if necessary
|
||||
if (page.imageUrl.isNullOrEmpty()) {
|
||||
page.status = Page.State.LoadPage
|
||||
try {
|
||||
page.imageUrl = download.source.getImageUrl(page)
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.Error(e)
|
||||
}
|
||||
pageList.asFlow().flatMapMerge(concurrency = downloadPreferences.parallelPageLimit.get()) { page ->
|
||||
flow {
|
||||
// Fetch image URL if necessary
|
||||
if (page.imageUrl.isNullOrEmpty()) {
|
||||
page.status = Page.State.LoadPage
|
||||
try {
|
||||
page.imageUrl = download.source.getImageUrl(page)
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
withIOContext { getOrDownloadImage(page, download, tmpDir) }
|
||||
emit(page)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
withIOContext { getOrDownloadImage(page, download, tmpDir) }
|
||||
emit(page)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
.collect {
|
||||
// Do when page is downloaded.
|
||||
notifier.onProgressChange(download)
|
||||
@@ -397,7 +406,7 @@ class Downloader(
|
||||
)
|
||||
|
||||
// Only rename the directory if it's downloaded
|
||||
if (downloadPreferences.saveChaptersAsCBZ().get()) {
|
||||
if (downloadPreferences.saveChaptersAsCBZ.get()) {
|
||||
archiveChapter(mangaDir, chapterDirname, tmpDir)
|
||||
} else {
|
||||
tmpDir.renameTo(chapterDirname)
|
||||
@@ -536,7 +545,7 @@ class Downloader(
|
||||
}
|
||||
|
||||
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) {
|
||||
if (!downloadPreferences.splitTallImages().get()) return
|
||||
if (!downloadPreferences.splitTallImages.get()) return
|
||||
|
||||
try {
|
||||
val filenamePrefix = "%03d".format(Locale.ENGLISH, page.number)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user