Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -29,3 +29,4 @@ ktlint_standard_function-expression-body = disabled
|
|||||||
ktlint_standard_function-signature = disabled
|
ktlint_standard_function-signature = disabled
|
||||||
ktlint_standard_type-argument-comment = disabled
|
ktlint_standard_type-argument-comment = disabled
|
||||||
ktlint_standard_type-parameter-comment = disabled
|
ktlint_standard_type-parameter-comment = disabled
|
||||||
|
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
patreon: mihon
|
||||||
@@ -30,7 +30,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.19.4](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ body:
|
|||||||
label: Mihon version
|
label: Mihon version
|
||||||
description: You can find your Mihon version in **More → About**.
|
description: You can find your Mihon version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.19.2"
|
Example: "0.19.4"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.19.4](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -26,20 +26,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
|
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||||
|
|
||||||
- name: Check code format
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
@@ -48,16 +48,24 @@ jobs:
|
|||||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
|
id: unit_tests
|
||||||
run: ./gradlew testReleaseUnitTest
|
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
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: arm64-v8a-${{ github.sha }}
|
name: arm64-v8a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: mapping-${{ github.sha }}
|
name: mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/release
|
path: app/build/outputs/mapping/release
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
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
|
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
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: mihon
|
name: mihon
|
||||||
path: |
|
path: |
|
||||||
@@ -83,16 +83,16 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||||
with:
|
with:
|
||||||
cache-disabled: true
|
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
|
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: mihon-foss
|
name: mihon-foss
|
||||||
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
@@ -143,12 +143,12 @@ jobs:
|
|||||||
mihon-foss
|
mihon-foss
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||||
body: |
|
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!
|
||||||
|
|
||||||
<!-->
|
<!-->
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,57 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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)) ([#2977](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
|
## [v0.19.2] - 2025-11-02
|
||||||
### Added
|
### 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))
|
- 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))
|
||||||
@@ -433,7 +484,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
- 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))
|
- 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.2...main
|
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.4...main
|
||||||
|
[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.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.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.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.mihon"
|
applicationId = "app.mihon"
|
||||||
|
|
||||||
versionCode = 15
|
versionCode = 17
|
||||||
versionName = "0.19.2"
|
versionName = "0.19.4"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -159,12 +159,14 @@ kotlin {
|
|||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-Xannotation-default-target=param-property",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,6 +272,7 @@ dependencies {
|
|||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
implementation(libs.bundles.markdown)
|
implementation(libs.bundles.markdown)
|
||||||
|
implementation(libs.materialKolor)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
|||||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||||
import tachiyomi.domain.category.repository.CategoryRepository
|
import tachiyomi.domain.category.repository.CategoryRepository
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
@@ -157,6 +158,7 @@ class DomainModule : InjektModule {
|
|||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
addFactory { GetChapter(get()) }
|
addFactory { GetChapter(get()) }
|
||||||
addFactory { GetChaptersByMangaId(get()) }
|
addFactory { GetChaptersByMangaId(get()) }
|
||||||
|
addFactory { GetBookmarkedChaptersByMangaId(get()) }
|
||||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
|||||||
onCreate: () -> Unit,
|
onCreate: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
SmallExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||||
onClick = onCreate,
|
onClick = onCreate,
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
import androidx.compose.material3.TooltipBox
|
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.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
|||||||
|
|
||||||
actions.filterIsInstance<AppBar.Action>().map {
|
actions.filterIsInstance<AppBar.Action>().map {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(it.title)
|
Text(it.title)
|
||||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
|||||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||||
if (overflowActions.isNotEmpty()) {
|
if (overflowActions.isNotEmpty()) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
|||||||
// Don't show search action
|
// Don't show search action
|
||||||
} else if (searchQuery == null) {
|
} else if (searchQuery == null) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_search))
|
Text(stringResource(MR.strings.action_search))
|
||||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
|||||||
}
|
}
|
||||||
} else if (searchQuery.isNotEmpty()) {
|
} else if (searchQuery.isNotEmpty()) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(MR.strings.action_reset))
|
Text(stringResource(MR.strings.action_reset))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ fun relativeDateText(
|
|||||||
Instant.ofEpochMilli(dateEpochMillis),
|
Instant.ofEpochMilli(dateEpochMillis),
|
||||||
ZoneId.systemDefault(),
|
ZoneId.systemDefault(),
|
||||||
)
|
)
|
||||||
.takeIf { dateEpochMillis > 0L },
|
.takeIf { dateEpochMillis != 0L },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadDropdownMenu(
|
fun DownloadDropdownMenu(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
offset: DpOffset? = null,
|
offset: DpOffset? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
if (offset != null) {
|
if (offset != null) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
private fun DownloadDropdownMenuItems(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> 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_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
|
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||||
)
|
)
|
||||||
|
|
||||||
options.map { (downloadAction, string) ->
|
options.map { (downloadAction, string) ->
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package eu.kanade.presentation.manga
|
package eu.kanade.presentation.manga
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -71,7 +70,6 @@ import tachiyomi.domain.source.model.StubSource
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
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.PullRefresh
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -124,7 +122,7 @@ fun MangaScreen(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -255,7 +253,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -330,25 +328,23 @@ private fun MangaScreenSmallImpl(
|
|||||||
val isFABVisible = remember(chapters) {
|
val isFABVisible = remember(chapters) {
|
||||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
SmallExtendedFloatingActionButton(
|
||||||
visible = isFABVisible,
|
text = {
|
||||||
enter = fadeIn(),
|
val isReading = remember(state.chapters) {
|
||||||
exit = fadeOut(),
|
state.chapters.fastAny { it.chapter.read }
|
||||||
) {
|
}
|
||||||
ExtendedFloatingActionButton(
|
Text(
|
||||||
text = {
|
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||||
val isReading = remember(state.chapters) {
|
)
|
||||||
state.chapters.fastAny { it.chapter.read }
|
},
|
||||||
}
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
Text(
|
onClick = onContinueReading,
|
||||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
},
|
visible = isFABVisible,
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
alignment = Alignment.BottomEnd,
|
||||||
onClick = onContinueReading,
|
),
|
||||||
expanded = chapterListState.shouldExpandFAB(),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val topPadding = contentPadding.calculateTopPadding()
|
val topPadding = contentPadding.calculateTopPadding()
|
||||||
@@ -499,7 +495,7 @@ fun MangaScreenLargeImpl(
|
|||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -572,27 +568,25 @@ fun MangaScreenLargeImpl(
|
|||||||
val isFABVisible = remember(chapters) {
|
val isFABVisible = remember(chapters) {
|
||||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
SmallExtendedFloatingActionButton(
|
||||||
visible = isFABVisible,
|
text = {
|
||||||
enter = fadeIn(),
|
val isReading = remember(state.chapters) {
|
||||||
exit = fadeOut(),
|
state.chapters.fastAny { it.chapter.read }
|
||||||
) {
|
}
|
||||||
ExtendedFloatingActionButton(
|
Text(
|
||||||
text = {
|
text = stringResource(
|
||||||
val isReading = remember(state.chapters) {
|
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||||
state.chapters.fastAny { it.chapter.read }
|
),
|
||||||
}
|
)
|
||||||
Text(
|
},
|
||||||
text = stringResource(
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
onClick = onContinueReading,
|
||||||
),
|
expanded = chapterListState.shouldExpandFAB(),
|
||||||
)
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
},
|
visible = isFABVisible,
|
||||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
alignment = Alignment.BottomEnd,
|
||||||
onClick = onContinueReading,
|
),
|
||||||
expanded = chapterListState.shouldExpandFAB(),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
@@ -745,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onChapterClicked: (Chapter) -> Unit,
|
onChapterClicked: (Chapter) -> Unit,
|
||||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> 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,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
@@ -793,14 +787,14 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onChapterSelected(item, !item.selected, true, true)
|
onChapterSelected(item, !item.selected, true)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onChapterItemClick(
|
onChapterItemClick(
|
||||||
chapterItem = item,
|
chapterItem = item,
|
||||||
isAnyChapterSelected = isAnyChapterSelected,
|
isAnyChapterSelected = isAnyChapterSelected,
|
||||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
|||||||
NEXT_10_CHAPTERS,
|
NEXT_10_CHAPTERS,
|
||||||
NEXT_25_CHAPTERS,
|
NEXT_25_CHAPTERS,
|
||||||
UNREAD_CHAPTERS,
|
UNREAD_CHAPTERS,
|
||||||
|
BOOKMARKED_CHAPTERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class EditCoverAction {
|
enum class EditCoverAction {
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ fun MangaBottomActionMenu(
|
|||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
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 ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
@@ -252,10 +252,10 @@ fun LibraryBottomActionMenu(
|
|||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
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 ->
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
resetJob?.cancel()
|
resetJob?.cancel()
|
||||||
resetJob = scope.launch {
|
resetJob = scope.launch {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
|
|||||||
@@ -567,44 +567,47 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
@Composable
|
||||||
annotate = { content, child ->
|
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
markdownAnnotator(
|
||||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
annotate = { content, child ->
|
||||||
|
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||||
|
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||||
|
|
||||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||||
?.getUnescapedTextInNode(content)
|
|
||||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
|
||||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
|
||||||
?.getUnescapedTextInNode(content)
|
?.getUnescapedTextInNode(content)
|
||||||
?: return@markdownAnnotator false
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||||
|
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||||
|
?.getUnescapedTextInNode(content)
|
||||||
|
?: return@markdownAnnotator false
|
||||||
|
|
||||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||||
?.getUnescapedTextInNode(content).orEmpty()
|
?.getUnescapedTextInNode(content).orEmpty()
|
||||||
|
|
||||||
withLink(LinkAnnotation.Url(url = url)) {
|
withLink(LinkAnnotation.Url(url = url)) {
|
||||||
pushStyle(linkStyle)
|
pushStyle(linkStyle)
|
||||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||||
append(altText)
|
append(altText)
|
||||||
pop()
|
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) {
|
false
|
||||||
append(content.substring(child.startOffset, child.endOffset))
|
},
|
||||||
return@markdownAnnotator true
|
config = markdownAnnotatorConfig(
|
||||||
}
|
eolAsNewLine = true,
|
||||||
|
),
|
||||||
false
|
)
|
||||||
},
|
}
|
||||||
config = markdownAnnotatorConfig(
|
|
||||||
eolAsNewLine = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ object AboutScreen : Screen() {
|
|||||||
is GetApplicationRelease.Result.OsTooOld -> {
|
is GetApplicationRelease.Result.OsTooOld -> {
|
||||||
context.toast(MR.strings.update_check_eol)
|
context.toast(MR.strings.update_check_eol)
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toast(e.message)
|
context.toast(e.message)
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
|
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||||
LibrariesContainer(
|
LibrariesContainer(
|
||||||
|
libraries = libraries,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
|||||||
pref = screenModel.preferences.customBrightness(),
|
pref = screenModel.preferences.customBrightness(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Sets the brightness of the screen. Range is [-75, 100].
|
* 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 -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.
|
* From 1 to 100 it sets that value as brightness.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package eu.kanade.presentation.theme
|
package eu.kanade.presentation.theme
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialExpressiveTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
|||||||
isAmoled: Boolean,
|
isAmoled: Boolean,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
MaterialTheme(
|
val context = LocalContext.current
|
||||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
val isDark = isSystemInDarkTheme()
|
||||||
|
MaterialExpressiveTheme(
|
||||||
|
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||||
|
getThemeColorScheme(
|
||||||
|
context = context,
|
||||||
|
appTheme = appTheme,
|
||||||
|
isDark = isDark,
|
||||||
|
isAmoled = isAmoled,
|
||||||
|
)
|
||||||
|
},
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
private fun getThemeColorScheme(
|
private fun getThemeColorScheme(
|
||||||
|
context: Context,
|
||||||
appTheme: AppTheme,
|
appTheme: AppTheme,
|
||||||
|
isDark: Boolean,
|
||||||
isAmoled: Boolean,
|
isAmoled: Boolean,
|
||||||
): ColorScheme {
|
): ColorScheme {
|
||||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||||
MonetColorScheme(LocalContext.current)
|
MonetColorScheme(context)
|
||||||
} else {
|
} else {
|
||||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||||
}
|
}
|
||||||
return colorScheme.getColorScheme(
|
return colorScheme.getColorScheme(
|
||||||
isSystemInDarkTheme(),
|
isDark = isDark,
|
||||||
isAmoled,
|
isAmoled = isAmoled,
|
||||||
|
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
|||||||
private val surfaceContainerHigh = Color(0xFF131313)
|
private val surfaceContainerHigh = Color(0xFF131313)
|
||||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
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 (!isDark) return lightScheme
|
||||||
|
|
||||||
if (!isAmoled) return darkScheme
|
if (!isAmoled) return darkScheme
|
||||||
|
|
||||||
return darkScheme.copy(
|
val amoledScheme = darkScheme.copy(
|
||||||
background = Color.Black,
|
background = Color.Black,
|
||||||
onBackground = Color.White,
|
onBackground = Color.White,
|
||||||
surface = Color.Black,
|
surface = Color.Black,
|
||||||
onSurface = Color.White,
|
onSurface = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||||
|
|
||||||
|
return amoledScheme.copy(
|
||||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||||
surfaceContainerLowest = surfaceContainer,
|
surfaceContainerLowest = surfaceContainer,
|
||||||
surfaceContainerLow = surfaceContainer,
|
surfaceContainerLow = surfaceContainer,
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
package eu.kanade.presentation.theme.colorscheme
|
package eu.kanade.presentation.theme.colorscheme
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.UiModeManager
|
|
||||||
import android.app.WallpaperManager
|
import android.app.WallpaperManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.content.getSystemService
|
import com.materialkolor.PaletteStyle
|
||||||
import com.google.android.material.color.utilities.Hct
|
import com.materialkolor.dynamiccolor.ColorSpec
|
||||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
import com.materialkolor.ktx.DynamicScheme
|
||||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
import com.materialkolor.toColorScheme
|
||||||
import com.google.android.material.color.utilities.SchemeContent
|
|
||||||
import com.google.android.material.color.utilities.Score
|
|
||||||
|
|
||||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||||
|
|
||||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
?.primaryColor
|
?.primaryColor
|
||||||
?.toArgb()
|
?.toArgb()
|
||||||
if (seed != null) {
|
if (seed != null) {
|
||||||
MonetCompatColorScheme(context, seed)
|
MonetCompatColorScheme(Color(seed))
|
||||||
} else {
|
} else {
|
||||||
TachiyomiColorScheme
|
TachiyomiColorScheme
|
||||||
}
|
}
|
||||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
|
|
||||||
override val lightScheme
|
override val lightScheme
|
||||||
get() = monet.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)
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
|||||||
override val darkScheme = dynamicDarkColorScheme(context)
|
override val darkScheme = dynamicDarkColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||||
|
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun Int.toComposeColor(): Color = Color(this)
|
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||||
|
return DynamicScheme(
|
||||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
seedColor = seed,
|
||||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
isDark = dark,
|
||||||
val scheme = SchemeContent(
|
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||||
Hct.fromInt(seed),
|
style = PaletteStyle.Expressive,
|
||||||
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(),
|
|
||||||
)
|
)
|
||||||
|
.toColorScheme(isAmoled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.track
|
package eu.kanade.presentation.track
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.Clipboard
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
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.capitalize
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.intl.Locale
|
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.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboard: Clipboard = LocalClipboard.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||||
@@ -248,6 +251,7 @@ private fun SearchResultItem(
|
|||||||
val shape = RoundedCornerShape(16.dp)
|
val shape = RoundedCornerShape(16.dp)
|
||||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
|||||||
expanded = dropDownMenuExpanded,
|
expanded = dropDownMenuExpanded,
|
||||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||||
onCopyName = {
|
onCopyName = {
|
||||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
scope.launch {
|
||||||
|
val clipEntry = ClipData.newPlainText(
|
||||||
|
trackSearch.title,
|
||||||
|
trackSearch.title,
|
||||||
|
).toClipEntry()
|
||||||
|
clipboard.setClipEntry(clipEntry)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onOpenInBrowser = {
|
onOpenInBrowser = {
|
||||||
val url = trackSearch.tracking_url
|
val url = trackSearch.tracking_url
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package eu.kanade.presentation.track.components
|
package eu.kanade.presentation.track.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
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.foundation.layout.size
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
@@ -30,18 +26,13 @@ fun TrackLogoIcon(
|
|||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Image(
|
||||||
|
painter = painterResource(tracker.getLogo()),
|
||||||
|
contentDescription = tracker.name,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium),
|
||||||
.padding(4.dp),
|
)
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(tracker.getLogo()),
|
|
||||||
contentDescription = tracker.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewLightDark
|
@PreviewLightDark
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package eu.kanade.presentation.track.components
|
package eu.kanade.presentation.track.components
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.test.DummyTracker
|
import eu.kanade.test.DummyTracker
|
||||||
|
|
||||||
@@ -13,8 +11,6 @@ internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker>
|
|||||||
DummyTracker(
|
DummyTracker(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
name = "Dummy Tracker",
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
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.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
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.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.theme.active
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -54,8 +58,10 @@ fun UpdateScreen(
|
|||||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
|
onFilterClicked: () -> Unit,
|
||||||
|
hasActiveFilters: Boolean,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode) {
|
BackHandler(enabled = state.selectionMode) {
|
||||||
onSelectAll(false)
|
onSelectAll(false)
|
||||||
@@ -66,6 +72,8 @@ fun UpdateScreen(
|
|||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
onCalendarClicked = { onCalendarClicked() },
|
onCalendarClicked = { onCalendarClicked() },
|
||||||
onUpdateLibrary = { onUpdateLibrary() },
|
onUpdateLibrary = { onUpdateLibrary() },
|
||||||
|
onFilterClicked = { onFilterClicked() },
|
||||||
|
hasFilters = hasActiveFilters,
|
||||||
actionModeCounter = state.selected.size,
|
actionModeCounter = state.selected.size,
|
||||||
onSelectAll = { onSelectAll(true) },
|
onSelectAll = { onSelectAll(true) },
|
||||||
onInvertSelection = { onInvertSelection() },
|
onInvertSelection = { onInvertSelection() },
|
||||||
@@ -133,6 +141,8 @@ fun UpdateScreen(
|
|||||||
private fun UpdatesAppBar(
|
private fun UpdatesAppBar(
|
||||||
onCalendarClicked: () -> Unit,
|
onCalendarClicked: () -> Unit,
|
||||||
onUpdateLibrary: () -> Unit,
|
onUpdateLibrary: () -> Unit,
|
||||||
|
onFilterClicked: () -> Unit,
|
||||||
|
hasFilters: Boolean,
|
||||||
// For action mode
|
// For action mode
|
||||||
actionModeCounter: Int,
|
actionModeCounter: Int,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
@@ -147,6 +157,12 @@ private fun UpdatesAppBar(
|
|||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
persistentListOf(
|
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(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_view_upcoming),
|
title = stringResource(MR.strings.action_view_upcoming),
|
||||||
icon = Icons.Outlined.CalendarMonth,
|
icon = Icons.Outlined.CalendarMonth,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
|
|||||||
internal fun LazyListScope.updatesUiItems(
|
internal fun LazyListScope.updatesUiItems(
|
||||||
uiModels: List<UpdatesUiModel>,
|
uiModels: List<UpdatesUiModel>,
|
||||||
selectionMode: Boolean,
|
selectionMode: Boolean,
|
||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onClickUpdate: (UpdatesItem) -> Unit,
|
onClickUpdate: (UpdatesItem) -> Unit,
|
||||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||||
@@ -111,11 +111,11 @@ internal fun LazyListScope.updatesUiItems(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
when {
|
when {
|
||||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||||
else -> onClickUpdate(updatesItem)
|
else -> onClickUpdate(updatesItem)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
context(Context)
|
context(context: Context)
|
||||||
val Throwable.formattedMessage: String
|
val Throwable.formattedMessage: String
|
||||||
get() {
|
get() {
|
||||||
when (this) {
|
when (this) {
|
||||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||||
is UnknownHostException -> {
|
is UnknownHostException -> {
|
||||||
return if (!isOnline()) {
|
return if (!context.isOnline()) {
|
||||||
stringResource(MR.strings.exception_offline)
|
context.stringResource(MR.strings.exception_offline)
|
||||||
} else {
|
} 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 NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||||
}
|
}
|
||||||
return when (val className = this::class.simpleName) {
|
return when (val className = this::class.simpleName) {
|
||||||
"Exception", "IOException" -> message ?: className
|
"Exception", "IOException" -> message ?: className
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
// https://issuetracker.google.com/352584409
|
// https://issuetracker.google.com/352584409
|
||||||
context(LazyItemScope)
|
context(itemScope: LazyItemScope)
|
||||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||||
|
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
|||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -28,7 +29,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
||||||
import com.kevinnzou.web.AccompanistWebChromeClient
|
import com.kevinnzou.web.AccompanistWebChromeClient
|
||||||
@@ -36,12 +39,13 @@ import com.kevinnzou.web.AccompanistWebViewClient
|
|||||||
import com.kevinnzou.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
import com.kevinnzou.web.WebContent
|
import com.kevinnzou.web.WebContent
|
||||||
import com.kevinnzou.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
|
import com.kevinnzou.web.WebViewNavigator
|
||||||
import com.kevinnzou.web.WebViewState
|
import com.kevinnzou.web.WebViewState
|
||||||
import com.kevinnzou.web.rememberWebViewNavigator
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.getHtml
|
import eu.kanade.tachiyomi.util.system.getHtml
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -50,13 +54,13 @@ import tachiyomi.i18n.MR
|
|||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
class WebViewWindow(webContent: WebContent) {
|
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
|
||||||
var state by mutableStateOf(WebViewState(webContent))
|
var state by mutableStateOf(WebViewState(webContent))
|
||||||
var popupMessage: Message? = null
|
var popupMessage: Message? = null
|
||||||
private set
|
private set
|
||||||
var webView: WebView? = null
|
var webView: WebView? = null
|
||||||
|
|
||||||
constructor(popupMessage: Message) : this(WebContent.NavigatorOnly) {
|
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
|
||||||
this.popupMessage = popupMessage
|
this.popupMessage = popupMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,27 +76,20 @@ fun WebViewScreenContent(
|
|||||||
headers: Map<String, String> = emptyMap(),
|
headers: Map<String, String> = emptyMap(),
|
||||||
onUrlChange: (String) -> Unit = {},
|
onUrlChange: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val windowStack = remember {
|
val windowStack = remember {
|
||||||
mutableStateStackOf(
|
mutableStateStackOf(
|
||||||
WebViewWindow(
|
WebViewWindow(
|
||||||
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
||||||
|
WebViewNavigator(coroutineScope),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentWindow = windowStack.lastItemOrNull!!
|
val currentWindow = windowStack.lastItemOrNull!!
|
||||||
|
val navigator = currentWindow.navigator
|
||||||
|
|
||||||
val popState: (() -> Unit) = remember {
|
|
||||||
{
|
|
||||||
if (windowStack.size == 1) {
|
|
||||||
onNavigateUp()
|
|
||||||
} else {
|
|
||||||
windowStack.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val navigator = rememberWebViewNavigator()
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -133,21 +130,20 @@ fun WebViewScreenContent(
|
|||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?,
|
request: WebResourceRequest?,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
request?.let {
|
val url = request?.url?.toString() ?: return false
|
||||||
// Don't attempt to open blobs as webpages
|
|
||||||
if (it.url.toString().startsWith("blob:http")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore intents urls
|
// Ignore intents urls
|
||||||
if (it.url.toString().startsWith("intent://")) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with request, but with custom headers
|
|
||||||
view?.loadUrl(it.url.toString(), headers)
|
|
||||||
}
|
}
|
||||||
return super.shouldOverrideUrlLoading(view, request)
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +158,7 @@ fun WebViewScreenContent(
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
||||||
if (isUserGesture) {
|
if (isUserGesture) {
|
||||||
windowStack.push(WebViewWindow(resultMsg))
|
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -177,6 +173,18 @@ fun WebViewScreenContent(
|
|||||||
return webView
|
return webView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val popState = remember<() -> Unit> {
|
||||||
|
{
|
||||||
|
if (windowStack.size == 1) {
|
||||||
|
onNavigateUp()
|
||||||
|
} else {
|
||||||
|
windowStack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(windowStack.size > 1, popState)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Box {
|
Box {
|
||||||
@@ -184,7 +192,7 @@ fun WebViewScreenContent(
|
|||||||
AppBar(
|
AppBar(
|
||||||
title = currentWindow.state.pageTitle ?: initialTitle,
|
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||||
subtitle = currentUrl,
|
subtitle = currentUrl,
|
||||||
navigateUp = popState,
|
navigateUp = onNavigateUp,
|
||||||
navigationIcon = Icons.Outlined.Close,
|
navigationIcon = Icons.Outlined.Close,
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
@@ -225,7 +233,18 @@ fun WebViewScreenContent(
|
|||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
onClick = { onClearCookies(currentUrl) },
|
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(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -254,7 +273,7 @@ fun WebViewScreenContent(
|
|||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
)
|
)
|
||||||
is LoadingState.Loading -> LinearProgressIndicator(
|
is LoadingState.Loading -> LinearProgressIndicator(
|
||||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
progress = { loadingState.progress },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
@@ -298,7 +317,7 @@ fun WebViewScreenContent(
|
|||||||
// The composable is being disposed but the WebView object is not.
|
// 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
|
// 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.
|
// before it was unmounted, we won't want it to reset back to its original target.
|
||||||
window.state = WebViewState(WebContent.NavigatorOnly)
|
window.state.content = WebContent.NavigatorOnly
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
client = webClient,
|
client = webClient,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.memory.MemoryCache
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import coil3.request.allowRgb565
|
import coil3.request.allowRgb565
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
@@ -157,8 +158,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// Updates widget update
|
// Updates widget update
|
||||||
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
||||||
|
|
||||||
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
val minLogPriority = when {
|
||||||
|
networkPreferences.verboseLogging().get() -> LogPriority.VERBOSE
|
||||||
|
BuildConfig.DEBUG -> LogPriority.DEBUG
|
||||||
|
else -> LogPriority.INFO
|
||||||
|
}
|
||||||
|
LogcatLogger.install()
|
||||||
|
LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority)
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeMigrator()
|
initializeMigrator()
|
||||||
@@ -196,6 +203,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
add(MangaKeyer())
|
add(MangaKeyer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoryCache(
|
||||||
|
MemoryCache.Builder()
|
||||||
|
.maxSizePercent(context)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||||
|
|||||||
@@ -418,10 +418,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
*/
|
*/
|
||||||
private const val KEY_CATEGORY = "category"
|
private const val KEY_CATEGORY = "category"
|
||||||
|
|
||||||
fun cancelAllWorks(context: Context) {
|
|
||||||
context.workManager.cancelAllWorkByTag(TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupTask(
|
fun setupTask(
|
||||||
context: Context,
|
context: Context,
|
||||||
prefInterval: Int? = null,
|
prefInterval: Int? = null,
|
||||||
@@ -435,16 +431,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
} else {
|
} else {
|
||||||
NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
}
|
}
|
||||||
val networkRequestBuilder = NetworkRequest.Builder()
|
val networkRequest = NetworkRequest.Builder().apply {
|
||||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||||
}
|
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
}
|
||||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||||
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.build()
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
.setRequiresBatteryNotLow(true)
|
.setRequiresBatteryNotLow(true)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -24,9 +23,6 @@ interface Tracker {
|
|||||||
|
|
||||||
val supportsPrivateTracking: Boolean
|
val supportsPrivateTracking: Boolean
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getLogoColor(): Int
|
|
||||||
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
fun getLogo(): Int
|
fun getLogo(): Int
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@@ -56,9 +55,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_anilist
|
override fun getLogo() = R.drawable.brand_anilist
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
|
||||||
| id
|
| id
|
||||||
| status
|
| status
|
||||||
|}
|
|}
|
||||||
@@ -78,14 +78,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation UpdateManga(
|
|mutation UpdateManga(
|
||||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|
||||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|
||||||
|) {
|
|) {
|
||||||
|SaveMediaListEntry(
|
|SaveMediaListEntry(
|
||||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
|id: $listId, progress: $progress, status: $status, private: $private,
|
||||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|
||||||
|) {
|
|) {
|
||||||
|id
|
|id
|
||||||
|status
|
|status
|
||||||
@@ -114,9 +114,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun deleteLibManga(track: DomainTrack) {
|
suspend fun deleteLibManga(track: DomainTrack) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|mutation DeleteManga(${'$'}listId: Int) {
|
|mutation DeleteManga($listId: Int) {
|
||||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
|DeleteMediaListEntry(id: $listId) {
|
||||||
|deleted
|
|deleted
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
@@ -135,10 +135,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query Search(${'$'}query: String) {
|
|query Search($query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
|id
|
|id
|
||||||
|staff {
|
|staff {
|
||||||
|edges {
|
|edges {
|
||||||
@@ -197,10 +197,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
|
|
||||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query ($id: Int!, $manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|
||||||
|id
|
|id
|
||||||
|status
|
|status
|
||||||
|scoreRaw: score(format: POINT_100)
|
|scoreRaw: score(format: POINT_100)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
package eu.kanade.tachiyomi.data.track.bangumi
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -79,9 +78,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_bangumi
|
override fun getLogo() = R.drawable.brand_bangumi
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(240, 145, 153)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
@@ -111,7 +108,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
// Users can set a 'username' (not nickname) once which effectively
|
// Users can set a 'username' (not nickname) once which effectively
|
||||||
// replaces the stringified ID in certain queries.
|
// replaces the stringified ID in certain queries.
|
||||||
// If no username is set, the API returns the user ID as a strings
|
// If no username is set, the API returns the user ID as a strings
|
||||||
var username = api.getUsername()
|
val username = api.getUsername()
|
||||||
saveCredentials(username, oauth.accessToken)
|
saveCredentials(username, oauth.accessToken)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kavita
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -34,9 +33,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
override fun getLogo(): Int = R.drawable.brand_kavita
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(74, 198, 148)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||||
|
|
||||||
@@ -140,7 +137,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authentication.apiUrl = prefApiUrl
|
authentication.apiUrl = prefApiUrl
|
||||||
authentication.jwtToken = token.toString()
|
authentication.jwtToken = token
|
||||||
}
|
}
|
||||||
authentications = oauth
|
authentications = oauth
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -36,9 +35,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
|
|
||||||
private val api by lazy { KitsuApi(client, interceptor) }
|
private val api by lazy { KitsuApi(client, interceptor) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_kitsu
|
override fun getLogo() = R.drawable.brand_kitsu
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.komga
|
package eu.kanade.tachiyomi.data.track.komga
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -31,9 +30,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
|
|||||||
|
|
||||||
val api by lazy { KomgaApi(id, client) }
|
val api by lazy { KomgaApi(id, client) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
override fun getLogo() = R.drawable.brand_komga
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.mangaupdates
|
package eu.kanade.tachiyomi.data.track.mangaupdates
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -42,9 +41,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
|
|
||||||
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
||||||
|
|
||||||
override fun getLogo(): Int = R.drawable.ic_manga_updates
|
override fun getLogo(): Int = R.drawable.brand_mangaupdates
|
||||||
|
|
||||||
override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
|
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -10,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -41,9 +39,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_mal
|
override fun getLogo() = R.drawable.brand_myanimelist
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
|
|||||||
@@ -10,15 +10,12 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga
|
|||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult
|
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.PkceUtil
|
import eu.kanade.tachiyomi.util.PkceUtil
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@@ -78,15 +75,15 @@ class MyAnimeListApi(
|
|||||||
// MAL API throws a 400 when the query is over 64 characters...
|
// MAL API throws a 400 when the query is over 64 characters...
|
||||||
.appendQueryParameter("q", query.take(64))
|
.appendQueryParameter("q", query.take(64))
|
||||||
.appendQueryParameter("nsfw", "true")
|
.appendQueryParameter("nsfw", "true")
|
||||||
|
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MALSearchResult>()
|
.parseAs<MALSearchResult>()
|
||||||
.data
|
.data
|
||||||
.map { async { getMangaDetails(it.node.id) } }
|
.filter { !(it.node.mediaType.contains("novel")) }
|
||||||
.awaitAll()
|
.map { parseSearchItem(it.node) }
|
||||||
.filter { !it.publishing_type.contains("novel") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,29 +92,13 @@ class MyAnimeListApi(
|
|||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||||
.appendPath(id.toString())
|
.appendPath(id.toString())
|
||||||
.appendQueryParameter(
|
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||||
"fields",
|
|
||||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MALManga>()
|
.parseAs<MALManga>()
|
||||||
.let {
|
.let { parseSearchItem(it) }
|
||||||
TrackSearch.create(trackId).apply {
|
|
||||||
remote_id = it.id
|
|
||||||
title = it.title
|
|
||||||
summary = it.synopsis
|
|
||||||
total_chapters = it.numChapters
|
|
||||||
score = it.mean
|
|
||||||
cover_url = it.covers?.large.orEmpty()
|
|
||||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
|
||||||
publishing_status = it.status.replace("_", " ")
|
|
||||||
publishing_type = it.mediaType.replace("_", " ")
|
|
||||||
start_date = it.startDate ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +162,7 @@ class MyAnimeListApi(
|
|||||||
|
|
||||||
val matches = myListSearchResult.data
|
val matches = myListSearchResult.data
|
||||||
.filter { it.node.title.contains(query, ignoreCase = true) }
|
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||||
.map { async { getMangaDetails(it.node.id) } }
|
.map { parseSearchItem(it.node) }
|
||||||
.awaitAll()
|
|
||||||
|
|
||||||
// Check next page if there's more
|
// Check next page if there's more
|
||||||
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
||||||
@@ -193,10 +173,10 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getListPage(offset: Int): MALUserSearchResult {
|
private suspend fun getListPage(offset: Int): MALSearchResult {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
|
||||||
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||||
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
|
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
urlBuilder.appendQueryParameter("offset", offset.toString())
|
urlBuilder.appendQueryParameter("offset", offset.toString())
|
||||||
@@ -225,6 +205,28 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
|
||||||
|
return TrackSearch.create(trackId).apply {
|
||||||
|
remote_id = searchItem.id
|
||||||
|
title = searchItem.title
|
||||||
|
summary = searchItem.synopsis
|
||||||
|
total_chapters = searchItem.numChapters
|
||||||
|
score = searchItem.mean
|
||||||
|
cover_url = searchItem.covers?.large.orEmpty()
|
||||||
|
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||||
|
publishing_status = searchItem.status.replace("_", " ")
|
||||||
|
publishing_type = searchItem.mediaType.replace("_", " ")
|
||||||
|
start_date = searchItem.startDate ?: ""
|
||||||
|
artists = searchItem.authors
|
||||||
|
.filter { authorNode -> authorNode.role == "Art" }
|
||||||
|
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||||
|
authors = searchItem.authors
|
||||||
|
// count all with "Story" or "Story & Art" as authors, like is done for library entries
|
||||||
|
.filter { authorNode -> authorNode.role.contains("Story") }
|
||||||
|
.mapNotNull { authorNode -> authorNode.node.getFullName() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseDate(isoDate: String): Long {
|
private fun parseDate(isoDate: String): Long {
|
||||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||||
}
|
}
|
||||||
@@ -236,7 +238,7 @@ class MyAnimeListApi(
|
|||||||
return try {
|
return try {
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
outputDf.format(epochTime)
|
outputDf.format(epochTime)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,6 +249,9 @@ class MyAnimeListApi(
|
|||||||
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
|
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
|
||||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
||||||
|
|
||||||
|
private const val SEARCH_FIELDS =
|
||||||
|
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date,authors{first_name,last_name}"
|
||||||
|
|
||||||
private const val LIST_PAGINATION_AMOUNT = 250
|
private const val LIST_PAGINATION_AMOUNT = 250
|
||||||
|
|
||||||
private var codeVerifier: String = ""
|
private var codeVerifier: String = ""
|
||||||
|
|||||||
@@ -18,8 +18,26 @@ data class MALManga(
|
|||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
@SerialName("start_date")
|
@SerialName("start_date")
|
||||||
val startDate: String?,
|
val startDate: String?,
|
||||||
|
val authors: List<MALAuthorNode> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALAuthorNode(
|
||||||
|
val node: MALAuthor,
|
||||||
|
val role: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALAuthor(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("first_name")
|
||||||
|
val firstName: String,
|
||||||
|
@SerialName("last_name")
|
||||||
|
val lastName: String,
|
||||||
|
) {
|
||||||
|
fun getFullName(): String? = "$firstName $lastName".trim().ifBlank { null }
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALMangaCovers(
|
data class MALMangaCovers(
|
||||||
val large: String = "",
|
val large: String = "",
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResult(
|
data class MALSearchResult(
|
||||||
val data: List<MALSearchResultNode>,
|
val data: List<MALSearchResultNode>,
|
||||||
|
val paging: MALSearchPaging,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResultNode(
|
data class MALSearchResultNode(
|
||||||
val node: MALSearchResultItem,
|
val node: MALManga,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALSearchResultItem(
|
data class MALSearchPaging(
|
||||||
val id: Int,
|
val next: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MALUserSearchResult(
|
|
||||||
val data: List<MALUserSearchItem>,
|
|
||||||
val paging: MALUserSearchPaging,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MALUserSearchItem(
|
|
||||||
val node: MALUserSearchItemNode,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MALUserSearchPaging(
|
|
||||||
val next: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MALUserSearchItemNode(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.shikimori
|
package eu.kanade.tachiyomi.data.track.shikimori
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -10,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -98,9 +96,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
override fun getLogo() = R.drawable.brand_shikimori
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> {
|
override fun getStatusList(): List<Long> {
|
||||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -18,14 +17,15 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
|
|
||||||
val api by lazy { SuwayomiApi(id) }
|
val api by lazy { SuwayomiApi(id) }
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
override fun getLogo() = R.drawable.brand_suwayomi
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNREAD = 1L
|
const val UNREAD = 1L
|
||||||
const val READING = 2L
|
const val READING = 2L
|
||||||
const val COMPLETED = 3L
|
const val COMPLETED = 3L
|
||||||
|
|
||||||
|
private const val TRACKER_DELETE_KEY = "Tracker Delete"
|
||||||
|
private const val TRACKER_DELETE_DEFAULT = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||||
@@ -58,7 +58,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.updateProgress(track)
|
return api.updateProgress(track, getPrefTrackerDelete())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||||
@@ -105,4 +105,9 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
|
|
||||||
private fun String.getMangaId(): Long =
|
private fun String.getMangaId(): Long =
|
||||||
this.substringAfterLast('/').toLong()
|
this.substringAfterLast('/').toLong()
|
||||||
|
|
||||||
|
private fun getPrefTrackerDelete(): Boolean {
|
||||||
|
val preferences = api.sourcePreferences()
|
||||||
|
return preferences.getBoolean(TRACKER_DELETE_KEY, TRACKER_DELETE_DEFAULT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.addAll
|
import kotlinx.serialization.json.addAll
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
@@ -29,19 +32,22 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
|
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
|
||||||
|
private val configurableSource: ConfigurableSource by lazy { (sourceManager.get(sourceId) as ConfigurableSource) }
|
||||||
private val client: OkHttpClient by lazy { source.client }
|
private val client: OkHttpClient by lazy { source.client }
|
||||||
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||||
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
||||||
|
|
||||||
|
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
|
||||||
|
|
||||||
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||||
val query = """
|
val query = $$"""
|
||||||
|query GetManga(${'$'}mangaId: Int!) {
|
|query GetManga($mangaId: Int!) {
|
||||||
| manga(id: ${'$'}mangaId) {
|
| manga(id: $mangaId) {
|
||||||
| ...MangaFragment
|
| ...MangaFragment
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
|
|
|
|
||||||
|$MangaFragment
|
|$$MangaFragment
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
val payload = buildJsonObject {
|
val payload = buildJsonObject {
|
||||||
put("query", query)
|
put("query", query)
|
||||||
@@ -79,12 +85,14 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateProgress(track: Track): Track {
|
suspend fun updateProgress(track: Track, deleteDownloadsOnServer: Boolean = false): Track {
|
||||||
val mangaId = track.remote_id
|
val mangaId = track.remote_id
|
||||||
|
|
||||||
val chaptersQuery = """
|
// TODO: Include a filter on the chapter number here
|
||||||
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
|
||||||
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
val chaptersQuery = $$"""
|
||||||
|
|query GetMangaUnreadChapters($mangaId: Int!) {
|
||||||
|
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
|
||||||
| nodes {
|
| nodes {
|
||||||
| id
|
| id
|
||||||
| chapterNumber
|
| chapterNumber
|
||||||
@@ -110,18 +118,29 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
.data
|
.data
|
||||||
.entry
|
.entry
|
||||||
.nodes
|
.nodes
|
||||||
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
|
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read + 0.001 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
val markQuery = """
|
val markQuery = if (deleteDownloadsOnServer) {
|
||||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
$$"""
|
||||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||||
| chapters {
|
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||||
| id
|
| __typename
|
||||||
| }
|
| }
|
||||||
| }
|
| deleteDownloadedChapters(input: {ids: $chapters}) {
|
||||||
|}
|
| __typename
|
||||||
""".trimMargin()
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
} else {
|
||||||
|
$$"""
|
||||||
|
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||||
|
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||||
|
| __typename
|
||||||
|
| }
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
}
|
||||||
val markPayload = buildJsonObject {
|
val markPayload = buildJsonObject {
|
||||||
put("query", markQuery)
|
put("query", markQuery)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
@@ -140,12 +159,10 @@ class SuwayomiApi(private val trackId: Long) {
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackQuery = """
|
val trackQuery = $$"""
|
||||||
|mutation TrackManga(${'$'}mangaId: Int!) {
|
|mutation TrackManga($mangaId: Int!) {
|
||||||
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
| trackProgress(input: {mangaId: $mangaId}) {
|
||||||
| trackRecords {
|
| __typename
|
||||||
| lastChapterRead
|
|
||||||
| }
|
|
||||||
| }
|
| }
|
||||||
|}
|
|}
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
@@ -46,6 +47,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
LibraryPreferences(get())
|
LibraryPreferences(get())
|
||||||
}
|
}
|
||||||
|
addSingletonFactory {
|
||||||
|
UpdatesPreferences(get())
|
||||||
|
}
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
ReaderPreferences(get())
|
ReaderPreferences(get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
session.fsync(outputStream)
|
session.fsync(outputStream)
|
||||||
}
|
}
|
||||||
|
service.contentResolver.delete(entry.uri, null, null)
|
||||||
|
|
||||||
val intentSender = PendingIntent.getBroadcast(
|
val intentSender = PendingIntent.getBroadcast(
|
||||||
service,
|
service,
|
||||||
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||||
).intentSender
|
).intentSender
|
||||||
|
@SuppressLint("RequestInstallPackagesPolicy")
|
||||||
session.commit(intentSender)
|
session.commit(intentSender)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun processEntry(entry: Entry) {
|
override fun processEntry(entry: Entry) {
|
||||||
super.processEntry(entry)
|
super.processEntry(entry)
|
||||||
try {
|
try {
|
||||||
shellInterface?.install(
|
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
|
||||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
shellInterface?.install(it)
|
||||||
)
|
}
|
||||||
|
service.contentResolver.delete(entry.uri, null, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||||
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
if (Shizuku.pingBinder()) {
|
||||||
|
try {
|
||||||
|
Shizuku.unbindUserService(shizukuArgs, connection, true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Failed to unbind shizuku service" }
|
||||||
|
}
|
||||||
|
}
|
||||||
service.unregisterReceiver(receiver)
|
service.unregisterReceiver(receiver)
|
||||||
logcat { "ShizukuInstaller destroy" }
|
logcat { "ShizukuInstaller destroy" }
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
intent.data?.let { contentResolver.delete(it, null, null) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager = Injekt.get<ExtensionManager>()
|
||||||
|
|||||||
@@ -1,66 +1,48 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.merge
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
internal class ExtensionInstaller(private val context: Context) {
|
internal class ExtensionInstaller(
|
||||||
|
private val context: Context,
|
||||||
/**
|
) {
|
||||||
* The system's download manager
|
|
||||||
*/
|
|
||||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The broadcast receiver which listens to download completion events.
|
|
||||||
*/
|
|
||||||
private val downloadReceiver = DownloadCompletionReceiver()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
|
||||||
* returned by the download manager.
|
|
||||||
*/
|
|
||||||
private val activeDownloads = hashMapOf<String, Long>()
|
|
||||||
|
|
||||||
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
|
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private val activeJobs = mutableMapOf<String, Job>()
|
||||||
|
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||||
|
|
||||||
|
private val httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||||
* step in the installation process.
|
* step in the installation process.
|
||||||
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param extension The extension to install.
|
* @param extension The extension to install.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
|
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
|
||||||
val pkgName = extension.pkgName
|
val downloadId = extension.pkgName.hashCode().toLong()
|
||||||
|
cancelInstall(extension.pkgName)
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val step = MutableStateFlow(InstallStep.Pending)
|
||||||
if (oldDownload != null) {
|
activeSteps[downloadId] = step
|
||||||
deleteDownload(pkgName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the receiver after removing (and unregistering) the previous download
|
val job = scope.launch {
|
||||||
downloadReceiver.register()
|
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
|
||||||
|
try {
|
||||||
|
step.value = InstallStep.Downloading
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val response = httpClient.newCall(request).execute()
|
||||||
|
|
||||||
val downloadUri = url.toUri()
|
if (!response.isSuccessful) {
|
||||||
val request = DownloadManager.Request(downloadUri)
|
throw Exception("Failed to download extension")
|
||||||
.setTitle(extension.name)
|
}
|
||||||
.setMimeType(APK_MIME)
|
response.body.byteStream().use { input ->
|
||||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
tmpFile.outputStream().use { output ->
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val id = downloadManager.enqueue(request)
|
step.value = InstallStep.Installing
|
||||||
activeDownloads[pkgName] = id
|
installApk(downloadId, tmpFile)
|
||||||
|
} catch (e: Exception) {
|
||||||
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
if (e is InterruptedException) {
|
||||||
downloadsStateFlows[id] = downloadStateFlow
|
// Canceled
|
||||||
|
} else {
|
||||||
// Poll download status
|
logcat(LogPriority.ERROR, e)
|
||||||
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
|
step.value = InstallStep.Error
|
||||||
// Map to our model
|
}
|
||||||
when (downloadStatus) {
|
|
||||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
|
||||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
|
activeJobs[extension.pkgName] = job
|
||||||
emit(it)
|
|
||||||
// Stop when the application is installed or errors
|
return step.asStateFlow()
|
||||||
!it.isCompleted()
|
.onCompletion {
|
||||||
}.onCompletion {
|
activeJobs.remove(extension.pkgName)
|
||||||
// Always notify on main thread
|
activeSteps.remove(downloadId)
|
||||||
withUIContext {
|
job.cancel()
|
||||||
// Always remove the download when unsubscribed
|
|
||||||
deleteDownload(pkgName)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a flow that polls the given download id for its status every second, as the
|
|
||||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
|
||||||
*
|
|
||||||
* @param id The id of the download to poll.
|
|
||||||
*/
|
|
||||||
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
|
|
||||||
val query = DownloadManager.Query().setFilterById(id)
|
|
||||||
while (true) {
|
|
||||||
// Get the current download status
|
|
||||||
val downloadStatus = downloadManager.query(query).use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) return@flow
|
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(downloadStatus)
|
|
||||||
|
|
||||||
// Stop polling when the download fails or finishes
|
|
||||||
if (
|
|
||||||
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
|
|
||||||
downloadStatus == DownloadManager.STATUS_FAILED
|
|
||||||
) {
|
|
||||||
return@flow
|
|
||||||
}
|
|
||||||
|
|
||||||
delay(1.seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ignore duplicate results
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts an intent to install the extension at the given uri.
|
* Starts an intent to install the extension at the given uri.
|
||||||
*
|
*
|
||||||
* @param uri The uri of the extension to install.
|
* @param tempFile The file of the extension to install. Delete after use.
|
||||||
*/
|
*/
|
||||||
fun installApk(downloadId: Long, uri: Uri) {
|
private fun installApk(downloadId: Long, tempFile: File) {
|
||||||
when (val installer = extensionInstaller.get()) {
|
when (val installer = extensionInstaller.get()) {
|
||||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||||
.setDataAndType(uri, APK_MIME)
|
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
BasePreferences.ExtensionInstaller.PRIVATE -> {
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
|
||||||
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
|
||||||
|
|
||||||
if (tempFile.exists() && !tempFile.delete()) {
|
|
||||||
// Unlikely but just in case
|
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
|
||||||
tempFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
updateInstallStep(downloadId, InstallStep.Installed)
|
||||||
} else {
|
} else {
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
updateInstallStep(downloadId, InstallStep.Error)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
updateInstallStep(downloadId, InstallStep.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
val intent = ExtensionInstallService.getIntent(
|
||||||
|
context,
|
||||||
|
downloadId,
|
||||||
|
tempFile.getUriCompat(context),
|
||||||
|
installer,
|
||||||
|
)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,9 +140,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* Cancels extension install and remove from download manager and installer.
|
* Cancels extension install and remove from download manager and installer.
|
||||||
*/
|
*/
|
||||||
fun cancelInstall(pkgName: String) {
|
fun cancelInstall(pkgName: String) {
|
||||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
activeJobs.remove(pkgName)?.cancel()
|
||||||
downloadManager.remove(downloadId)
|
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
|
||||||
Installer.cancelInstallQueue(context, downloadId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param step New install step.
|
* @param step New install step.
|
||||||
*/
|
*/
|
||||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||||
downloadsStateFlows[downloadId]?.let { it.value = step }
|
activeSteps[downloadId]?.let { it.value = step }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the download for the given package name.
|
|
||||||
*
|
|
||||||
* @param pkgName The package name of the download to delete.
|
|
||||||
*/
|
|
||||||
private fun deleteDownload(pkgName: String) {
|
|
||||||
val downloadId = activeDownloads.remove(pkgName)
|
|
||||||
if (downloadId != null) {
|
|
||||||
downloadManager.remove(downloadId)
|
|
||||||
downloadsStateFlows.remove(downloadId)
|
|
||||||
}
|
|
||||||
if (activeDownloads.isEmpty()) {
|
|
||||||
downloadReceiver.unregister()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receiver that listens to download status events.
|
|
||||||
*/
|
|
||||||
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this receiver is currently registered.
|
|
||||||
*/
|
|
||||||
private var isRegistered = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this receiver if it's not already.
|
|
||||||
*/
|
|
||||||
fun register() {
|
|
||||||
if (isRegistered) return
|
|
||||||
isRegistered = true
|
|
||||||
|
|
||||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
||||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters this receiver if it's not already.
|
|
||||||
*/
|
|
||||||
fun unregister() {
|
|
||||||
if (!isRegistered) return
|
|
||||||
isRegistered = false
|
|
||||||
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a download event is received. It looks for the download in the current active
|
|
||||||
* downloads and notifies its installation step.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
|
|
||||||
|
|
||||||
// Avoid events for downloads we didn't request
|
|
||||||
if (id !in activeDownloads.values) return
|
|
||||||
|
|
||||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
|
||||||
|
|
||||||
// Set next installation step
|
|
||||||
if (uri == null) {
|
|
||||||
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
|
|
||||||
updateInstallStep(id, InstallStep.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val query = DownloadManager.Query().setFilterById(id)
|
|
||||||
downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val localUri = cursor.getString(
|
|
||||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
|
||||||
).removePrefix(FILE_SCHEME)
|
|
||||||
|
|
||||||
installApk(id, File(localUri).getUriCompat(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val APK_MIME = "application/vnd.android.package-archive"
|
const val APK_MIME = "application/vnd.android.package-archive"
|
||||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||||
const val FILE_SCHEME = "file://"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,79 +50,46 @@ class ExtensionsScreenModel(
|
|||||||
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
|
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val queryFilter: (String) -> ((Extension) -> Boolean) = { query ->
|
|
||||||
filter@{ extension ->
|
|
||||||
if (query.isEmpty()) return@filter true
|
|
||||||
query.split(",").any { _input ->
|
|
||||||
val input = _input.trim()
|
|
||||||
if (input.isEmpty()) return@any false
|
|
||||||
when (extension) {
|
|
||||||
is Extension.Available -> {
|
|
||||||
extension.sources.any {
|
|
||||||
it.name.contains(input, ignoreCase = true) ||
|
|
||||||
it.baseUrl.contains(input, ignoreCase = true) ||
|
|
||||||
it.id == input.toLongOrNull()
|
|
||||||
} ||
|
|
||||||
extension.name.contains(input, ignoreCase = true)
|
|
||||||
}
|
|
||||||
is Extension.Installed -> {
|
|
||||||
extension.sources.any {
|
|
||||||
it.name.contains(input, ignoreCase = true) ||
|
|
||||||
it.id == input.toLongOrNull() ||
|
|
||||||
if (it is HttpSource) {
|
|
||||||
it.baseUrl.contains(input, ignoreCase = true)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} ||
|
|
||||||
extension.name.contains(input, ignoreCase = true)
|
|
||||||
}
|
|
||||||
is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
combine(
|
combine(
|
||||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
state.map { it.searchQuery }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.debounce(SEARCH_DEBOUNCE_MILLIS)
|
||||||
|
.map { searchQueryPredicate(it ?: "") },
|
||||||
currentDownloads,
|
currentDownloads,
|
||||||
getExtensions.subscribe(),
|
getExtensions.subscribe(),
|
||||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||||
val searchQuery = query ?: ""
|
buildMap {
|
||||||
|
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||||
val itemsGroups: ItemGroups = mutableMapOf()
|
if (updates.isNotEmpty()) {
|
||||||
|
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||||
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
|
||||||
if (updates.isNotEmpty()) {
|
|
||||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates
|
|
||||||
}
|
|
||||||
|
|
||||||
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
|
||||||
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
|
|
||||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
|
||||||
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = installed + untrusted
|
|
||||||
}
|
|
||||||
|
|
||||||
val languagesWithExtensions = _available
|
|
||||||
.filter(queryFilter(searchQuery))
|
|
||||||
.groupBy { it.lang }
|
|
||||||
.toSortedMap(LocaleHelper.comparator)
|
|
||||||
.map { (lang, exts) ->
|
|
||||||
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
|
||||||
exts.map(extensionMapper(downloads))
|
|
||||||
}
|
}
|
||||||
if (languagesWithExtensions.isNotEmpty()) {
|
|
||||||
itemsGroups.putAll(languagesWithExtensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsGroups
|
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
|
||||||
|
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
|
||||||
|
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||||
|
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
|
||||||
|
}
|
||||||
|
|
||||||
|
val languagesWithExtensions = _available
|
||||||
|
.filter(predicate)
|
||||||
|
.groupBy { it.lang }
|
||||||
|
.toSortedMap(LocaleHelper.comparator)
|
||||||
|
.map { (lang, exts) ->
|
||||||
|
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
|
||||||
|
exts.map(extensionMapper(downloads))
|
||||||
|
}
|
||||||
|
if (languagesWithExtensions.isNotEmpty()) {
|
||||||
|
putAll(languagesWithExtensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.collectLatest {
|
.collectLatest { items ->
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = it,
|
items = items,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
|
|||||||
.launchIn(screenModelScope)
|
.launchIn(screenModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
|
||||||
|
val subqueries = query.split(",")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filterNot { it.isBlank() }
|
||||||
|
|
||||||
|
if (subqueries.isEmpty()) return { true }
|
||||||
|
|
||||||
|
return { extension ->
|
||||||
|
subqueries.any { subquery ->
|
||||||
|
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
|
||||||
|
|
||||||
|
when (extension) {
|
||||||
|
is Extension.Installed -> extension.sources.any { source ->
|
||||||
|
source.name.contains(subquery, ignoreCase = true) ||
|
||||||
|
(source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true ||
|
||||||
|
source.id == subquery.toLongOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
is Extension.Available -> extension.sources.any {
|
||||||
|
it.name.contains(subquery, ignoreCase = true) ||
|
||||||
|
it.baseUrl.contains(subquery, ignoreCase = true) ||
|
||||||
|
it.id == subquery.toLongOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(searchQuery = query)
|
it.copy(searchQuery = query)
|
||||||
@@ -222,7 +219,7 @@ class ExtensionsScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias ItemGroups = MutableMap<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
typealias ItemGroups = Map<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
|
||||||
|
|
||||||
object ExtensionUiModel {
|
object ExtensionUiModel {
|
||||||
sealed interface Header {
|
sealed interface Header {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.extension
|
package eu.kanade.tachiyomi.ui.browse.extension
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -49,6 +50,10 @@ fun extensionsTab(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
content = { contentPadding, _ ->
|
content = { contentPadding, _ ->
|
||||||
|
BackHandler(enabled = state.searchQuery != null) {
|
||||||
|
extensionsScreenModel.search(null)
|
||||||
|
}
|
||||||
|
|
||||||
ExtensionScreen(
|
ExtensionScreen(
|
||||||
state = state,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
@@ -29,7 +32,6 @@ import mihon.feature.migration.config.MigrationConfigScreen
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (state.selectionMode) {
|
SmallExtendedFloatingActionButton(
|
||||||
ExtendedFloatingActionButton(
|
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
icon = {
|
||||||
icon = {
|
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
},
|
||||||
},
|
onClick = {
|
||||||
onClick = {
|
val selection = state.selection
|
||||||
val selection = state.selection
|
screenModel.clearSelection()
|
||||||
screenModel.clearSelection()
|
navigator.push(MigrationConfigScreen(selection))
|
||||||
navigator.push(MigrationConfigScreen(selection))
|
},
|
||||||
},
|
expanded = lazyListState.shouldExpandFAB(),
|
||||||
expanded = lazyListState.shouldExpandFAB(),
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
)
|
visible = state.selectionMode,
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
@@ -34,7 +37,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
|
|||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
@@ -73,13 +75,15 @@ data class MigrateSourceSearchScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
|
SmallExtendedFloatingActionButton(
|
||||||
ExtendedFloatingActionButton(
|
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
onClick = screenModel::openFilterSheet,
|
||||||
onClick = screenModel::openFilterSheet,
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
)
|
visible = state.filters.isNotEmpty(),
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -16,8 +13,10 @@ import androidx.compose.material.icons.outlined.Pause
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import tachiyomi.core.common.util.lang.launchUI
|
import tachiyomi.core.common.util.lang.launchUI
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.Pill
|
import tachiyomi.presentation.core.components.Pill
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||||
visible = downloadList.isNotEmpty(),
|
SmallExtendedFloatingActionButton(
|
||||||
enter = fadeIn(),
|
text = {
|
||||||
exit = fadeOut(),
|
val id = if (isRunning) {
|
||||||
) {
|
MR.strings.action_pause
|
||||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
} else {
|
||||||
ExtendedFloatingActionButton(
|
MR.strings.action_resume
|
||||||
text = {
|
}
|
||||||
val id = if (isRunning) {
|
Text(text = stringResource(id))
|
||||||
MR.strings.action_pause
|
},
|
||||||
} else {
|
icon = {
|
||||||
MR.strings.action_resume
|
val icon = if (isRunning) {
|
||||||
}
|
Icons.Outlined.Pause
|
||||||
Text(text = stringResource(id))
|
} else {
|
||||||
},
|
Icons.Filled.PlayArrow
|
||||||
icon = {
|
}
|
||||||
val icon = if (isRunning) {
|
Icon(imageVector = icon, contentDescription = null)
|
||||||
Icons.Outlined.Pause
|
},
|
||||||
} else {
|
onClick = {
|
||||||
Icons.Filled.PlayArrow
|
if (isRunning) {
|
||||||
}
|
screenModel.pauseDownloads()
|
||||||
Icon(imageVector = icon, contentDescription = null)
|
} else {
|
||||||
},
|
screenModel.startDownloads()
|
||||||
onClick = {
|
}
|
||||||
if (isRunning) {
|
},
|
||||||
screenModel.pauseDownloads()
|
expanded = fabExpanded,
|
||||||
} else {
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
screenModel.startDownloads()
|
visible = downloadList.isNotEmpty(),
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
},
|
),
|
||||||
expanded = fabExpanded,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (downloadList.isEmpty()) {
|
if (downloadList.isEmpty()) {
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
private const val LOCAL_SOURCE_ID_ALIAS = "local"
|
||||||
|
|
||||||
data class LibraryItem(
|
data class LibraryItem(
|
||||||
val libraryManga: LibraryManga,
|
val libraryManga: LibraryManga,
|
||||||
val downloadCount: Long = -1,
|
val downloadCount: Long = -1,
|
||||||
@@ -23,9 +26,17 @@ data class LibraryItem(
|
|||||||
* @return true if the manga matches the query, false otherwise.
|
* @return true if the manga matches the query, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun matches(constraint: String): Boolean {
|
fun matches(constraint: String): Boolean {
|
||||||
val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() }
|
val source = sourceManager.getOrStub(libraryManga.manga.source)
|
||||||
|
val sourceName by lazy { source.getNameForMangaInfo() }
|
||||||
if (constraint.startsWith("id:", true)) {
|
if (constraint.startsWith("id:", true)) {
|
||||||
return id == constraint.substringAfter("id:").toLongOrNull()
|
return id == constraint.substringAfter("id:").toLongOrNull()
|
||||||
|
} else if (constraint.startsWith("src:", true)) {
|
||||||
|
val querySource = constraint.substringAfter("src:")
|
||||||
|
return if (querySource.equals(LOCAL_SOURCE_ID_ALIAS, ignoreCase = true)) {
|
||||||
|
source.id == LocalSource.ID
|
||||||
|
} else {
|
||||||
|
source.id == querySource.toLongOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return libraryManga.manga.title.contains(constraint, true) ||
|
return libraryManga.manga.title.contains(constraint, true) ||
|
||||||
(libraryManga.manga.author?.contains(constraint, true) ?: false) ||
|
(libraryManga.manga.author?.contains(constraint, true) ?: false) ||
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
|
|||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
@@ -73,6 +74,7 @@ class LibraryScreenModel(
|
|||||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||||
|
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
|
||||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
@@ -234,9 +236,7 @@ class LibraryScreenModel(
|
|||||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||||
|
|
||||||
val mangaTracks = trackMap
|
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
|
||||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
|
||||||
.orEmpty()
|
|
||||||
|
|
||||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||||
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
|
||||||
@@ -467,15 +467,19 @@ class LibraryScreenModel(
|
|||||||
* Queues the amount specified of unread chapters from the list of selected manga
|
* Queues the amount specified of unread chapters from the list of selected manga
|
||||||
*/
|
*/
|
||||||
fun performDownloadAction(action: DownloadAction) {
|
fun performDownloadAction(action: DownloadAction) {
|
||||||
val mangas = state.value.selectedManga
|
when (action) {
|
||||||
val amount = when (action) {
|
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
|
||||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
|
||||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
|
||||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
|
||||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
|
||||||
DownloadAction.UNREAD_CHAPTERS -> null
|
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
|
||||||
}
|
}
|
||||||
clearSelection()
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadNextChapters(amount: Int?) {
|
||||||
|
val mangas = state.value.selectedManga
|
||||||
screenModelScope.launchNonCancellable {
|
screenModelScope.launchNonCancellable {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
val chapters = getNextChapters.await(manga.id)
|
val chapters = getNextChapters.await(manga.id)
|
||||||
@@ -496,6 +500,26 @@ class LibraryScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadBookmarkedChapters() {
|
||||||
|
val mangas = state.value.selectedManga
|
||||||
|
screenModelScope.launchNonCancellable {
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
val chapters = getBookmarkedChaptersByMangaId.await(manga.id)
|
||||||
|
.fastFilterNot { chapter ->
|
||||||
|
downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
|
||||||
|
downloadManager.isChapterDownloaded(
|
||||||
|
chapter.name,
|
||||||
|
chapter.scanlator,
|
||||||
|
chapter.url,
|
||||||
|
manga.title,
|
||||||
|
manga.source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks mangas' chapters read status.
|
* Marks mangas' chapters read status.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
@@ -59,12 +58,10 @@ import kotlinx.coroutines.launch
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.feature.migration.config.MigrationConfigScreen
|
import mihon.feature.migration.config.MigrationConfigScreen
|
||||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class MangaScreen(
|
class MangaScreen(
|
||||||
@@ -325,12 +322,7 @@ class MangaScreen(
|
|||||||
try {
|
try {
|
||||||
getMangaUrl(manga_, source_)?.let { url ->
|
getMangaUrl(manga_, source_)?.let { url ->
|
||||||
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
||||||
context.startActivity(
|
context.startActivity(intent)
|
||||||
Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
context.stringResource(MR.strings.action_share),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toast(e.message)
|
context.toast(e.message)
|
||||||
|
|||||||
@@ -650,6 +650,13 @@ class MangaScreenModel(
|
|||||||
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
|
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getBookmarkedChapters(): List<Chapter> {
|
||||||
|
val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty()
|
||||||
|
return chapterItems
|
||||||
|
.filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED }
|
||||||
|
.map { it.chapter }
|
||||||
|
}
|
||||||
|
|
||||||
private fun startDownload(
|
private fun startDownload(
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
startNow: Boolean,
|
startNow: Boolean,
|
||||||
@@ -712,6 +719,7 @@ class MangaScreenModel(
|
|||||||
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
||||||
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
||||||
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
||||||
|
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
|
||||||
}
|
}
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
if (chaptersToDownload.isNotEmpty()) {
|
||||||
startDownload(chaptersToDownload, false)
|
startDownload(chaptersToDownload, false)
|
||||||
@@ -953,7 +961,6 @@ class MangaScreenModel(
|
|||||||
fun toggleSelection(
|
fun toggleSelection(
|
||||||
item: ChapterList.Item,
|
item: ChapterList.Item,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
userSelected: Boolean = false,
|
|
||||||
fromLongPress: Boolean = false,
|
fromLongPress: Boolean = false,
|
||||||
) {
|
) {
|
||||||
updateSuccessState { successState ->
|
updateSuccessState { successState ->
|
||||||
@@ -968,7 +975,7 @@ class MangaScreenModel(
|
|||||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
selectedChapterIds.addOrRemove(item.id, selected)
|
selectedChapterIds.addOrRemove(item.id, selected)
|
||||||
|
|
||||||
if (selected && userSelected && fromLongPress) {
|
if (selected && fromLongPress) {
|
||||||
if (firstSelection) {
|
if (firstSelection) {
|
||||||
selectedPositions[0] = selectedIndex
|
selectedPositions[0] = selectedIndex
|
||||||
selectedPositions[1] = selectedIndex
|
selectedPositions[1] = selectedIndex
|
||||||
@@ -994,7 +1001,7 @@ class MangaScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (userSelected && !fromLongPress) {
|
} else if (!fromLongPress) {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
if (selectedIndex == selectedPositions[0]) {
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
selectedPositions[0] = indexOfFirst { it.selected }
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
|
|||||||
@@ -344,7 +344,9 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
viewModel.flushReadTimer()
|
lifecycleScope.launchNonCancellable {
|
||||||
|
viewModel.updateHistory()
|
||||||
|
}
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +578,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
private fun shareChapter() {
|
private fun shareChapter() {
|
||||||
assistUrl?.let {
|
assistUrl?.let {
|
||||||
val intent = it.toUri().toShareIntent(this, type = "text/plain")
|
val intent = it.toUri().toShareIntent(this, type = "text/plain")
|
||||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +727,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
message = stringResource(MR.strings.share_page_info, manga.title, chapter.name, page.number),
|
message = stringResource(MR.strings.share_page_info, manga.title, chapter.name, page.number),
|
||||||
)
|
)
|
||||||
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCopyImageResult(uri: Uri) {
|
private fun onCopyImageResult(uri: Uri) {
|
||||||
@@ -777,7 +779,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
* Updates viewer inset depending on fullscreen reader preferences.
|
* Updates viewer inset depending on fullscreen reader preferences.
|
||||||
*/
|
*/
|
||||||
private fun updateViewerInset(fullscreen: Boolean, drawUnderCutout: Boolean) {
|
private fun updateViewerInset(fullscreen: Boolean, drawUnderCutout: Boolean) {
|
||||||
val view = viewModel.state.value.viewer?.getView() ?: return
|
if (!::binding.isInitialized) return
|
||||||
|
val view = binding.viewerContainer
|
||||||
|
|
||||||
view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen, drawUnderCutout)
|
view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen, drawUnderCutout)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
|
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
|
||||||
@@ -832,7 +835,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
private val grayBackgroundColor = Color.rgb(0x20, 0x21, 0x25)
|
private val grayBackgroundColor = Color.rgb(0x20, 0x21, 0x25)
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Initializes the reader subscriptions.
|
* Initializes the reader subscriptions.
|
||||||
*/
|
*/
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
logcat { "Loading ${chapter.chapter.url}" }
|
logcat { "Loading ${chapter.chapter.url}" }
|
||||||
|
|
||||||
flushReadTimer()
|
updateHistory()
|
||||||
restartReadTimer()
|
restartReadTimer()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -501,7 +501,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
* if setting is enabled and [currentChapter] is queued for download
|
* if setting is enabled and [currentChapter] is queued for download
|
||||||
*/
|
*/
|
||||||
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
|
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
|
||||||
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also {
|
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!)?.also {
|
||||||
downloadManager.cancelQueuedDownloads(listOf(it))
|
downloadManager.cancelQueuedDownloads(listOf(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,26 +585,20 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
chapterReadStartTime = Instant.now().toEpochMilli()
|
chapterReadStartTime = Instant.now().toEpochMilli()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flushReadTimer() {
|
|
||||||
getCurrentChapter()?.let {
|
|
||||||
viewModelScope.launchNonCancellable {
|
|
||||||
updateHistory(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the chapter last read history if incognito mode isn't on.
|
* Saves the chapter last read history if incognito mode isn't on.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateHistory(readerChapter: ReaderChapter) {
|
suspend fun updateHistory() {
|
||||||
if (incognitoMode) return
|
getCurrentChapter()?.let { readerChapter ->
|
||||||
|
if (incognitoMode) return@let
|
||||||
|
|
||||||
val chapterId = readerChapter.chapter.id!!
|
val chapterId = readerChapter.chapter.id!!
|
||||||
val endTime = Date()
|
val endTime = Date()
|
||||||
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
||||||
|
|
||||||
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
||||||
chapterReadStartTime = null
|
chapterReadStartTime = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -655,7 +649,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
updateChapter.await(
|
updateChapter.await(
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
id = chapter.id!!.toLong(),
|
id = chapter.id!!,
|
||||||
bookmark = bookmarked,
|
bookmark = bookmarked,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,15 +51,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition
|
||||||
if (chapters.prevChapter != null) {
|
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
|
||||||
// selected as the current chapter when one of those pages is selected.
|
|
||||||
val prevPages = chapters.prevChapter.pages
|
|
||||||
if (prevPages != null) {
|
|
||||||
newItems.addAll(prevPages.takeLast(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
@@ -101,14 +94,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chapters.nextChapter != null) {
|
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||||
// Add at most two pages, because this chapter will be selected before the user can
|
|
||||||
// swap more pages.
|
|
||||||
val nextPages = chapters.nextChapter.pages
|
|
||||||
if (nextPages != null) {
|
|
||||||
newItems.addAll(nextPages.take(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resets double-page splits, else insert pages get misplaced
|
// Resets double-page splits, else insert pages get misplaced
|
||||||
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
|
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
|
||||||
@@ -123,7 +109,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
|
|
||||||
// Will skip insert page otherwise
|
// Will skip insert page otherwise
|
||||||
if (insertPageLastPage != null) {
|
if (insertPageLastPage != null) {
|
||||||
viewer.moveToPage(insertPageLastPage!!)
|
viewer.moveToPage(insertPageLastPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +180,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cleanupPageSplit() {
|
fun cleanupPageSplit() {
|
||||||
val insertPages = items.filterIsInstance(InsertPage::class.java)
|
val insertPages = items.filterIsInstance<InsertPage>()
|
||||||
items.removeAll(insertPages)
|
items.removeAll(insertPages)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
|||||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition.
|
||||||
if (chapters.prevChapter != null) {
|
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||||
// We only need to add the last few pages of the previous chapter, because it'll be
|
|
||||||
// selected as the current chapter when one of those pages is selected.
|
|
||||||
val prevPages = chapters.prevChapter.pages
|
|
||||||
if (prevPages != null) {
|
|
||||||
newItems.addAll(prevPages.takeLast(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
@@ -70,14 +63,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
|||||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chapters.nextChapter != null) {
|
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||||
// Add at most two pages, because this chapter will be selected before the user can
|
|
||||||
// swap more pages.
|
|
||||||
val nextPages = chapters.nextChapter.pages
|
|
||||||
if (nextPages != null) {
|
|
||||||
newItems.addAll(nextPages.take(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItems(newItems)
|
updateItems(newItems)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
|||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float,
|
velocityY: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
recycler?.onManualScroll()
|
||||||
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
|
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
|||||||
|
|
||||||
import android.animation.AnimatorSet
|
import android.animation.AnimatorSet
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
@@ -35,10 +34,6 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
private var firstVisibleItemPosition = 0
|
private var firstVisibleItemPosition = 0
|
||||||
private var lastVisibleItemPosition = 0
|
private var lastVisibleItemPosition = 0
|
||||||
private var currentScale = DEFAULT_RATE
|
private var currentScale = DEFAULT_RATE
|
||||||
|
|
||||||
private var isScrolling = false
|
|
||||||
private var hasTappedWhileScrolling = false
|
|
||||||
|
|
||||||
var zoomOutDisabled = false
|
var zoomOutDisabled = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -57,6 +52,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
var tapListener: ((MotionEvent) -> Unit)? = null
|
var tapListener: ((MotionEvent) -> Unit)? = null
|
||||||
var longTapListener: ((MotionEvent) -> Boolean)? = null
|
var longTapListener: ((MotionEvent) -> Boolean)? = null
|
||||||
|
|
||||||
|
private var isManuallyScrolling = false
|
||||||
|
private var tapDuringManualScroll = false
|
||||||
|
|
||||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||||
halfWidth = MeasureSpec.getSize(widthSpec) / 2
|
halfWidth = MeasureSpec.getSize(widthSpec) / 2
|
||||||
halfHeight = MeasureSpec.getSize(heightSpec) / 2
|
halfHeight = MeasureSpec.getSize(heightSpec) / 2
|
||||||
@@ -67,11 +65,11 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
super.onMeasure(widthSpec, heightSpec)
|
super.onMeasure(widthSpec, heightSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||||
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
|
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
hasTappedWhileScrolling = isScrolling
|
tapDuringManualScroll = isManuallyScrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
detector.onTouchEvent(e)
|
detector.onTouchEvent(e)
|
||||||
return super.onTouchEvent(e)
|
return super.onTouchEvent(e)
|
||||||
}
|
}
|
||||||
@@ -91,7 +89,10 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
val totalItemCount = layoutManager?.itemCount ?: 0
|
val totalItemCount = layoutManager?.itemCount ?: 0
|
||||||
atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
|
atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
|
||||||
atFirstPosition = firstVisibleItemPosition == 0
|
atFirstPosition = firstVisibleItemPosition == 0
|
||||||
isScrolling = state != SCROLL_STATE_IDLE
|
|
||||||
|
if (state == SCROLL_STATE_IDLE) {
|
||||||
|
isManuallyScrolling = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPositionX(positionX: Float): Float {
|
private fun getPositionX(positionX: Float): Float {
|
||||||
@@ -222,10 +223,14 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onManualScroll() {
|
||||||
|
isManuallyScrolling = true
|
||||||
|
}
|
||||||
|
|
||||||
inner class GestureListener : GestureDetectorWithLongTap.Listener() {
|
inner class GestureListener : GestureDetectorWithLongTap.Listener() {
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
|
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
|
||||||
if (!hasTappedWhileScrolling) {
|
if (!tapDuringManualScroll) {
|
||||||
tapListener?.invoke(ev)
|
tapListener?.invoke(ev)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
|||||||
|
|
||||||
internal val trackerManager: TrackerManager by injectLazy()
|
internal val trackerManager: TrackerManager by injectLazy()
|
||||||
|
|
||||||
abstract fun handleResult(data: Uri?)
|
abstract fun handleResult(uri: Uri)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -23,7 +23,12 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
|||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResult(intent.data)
|
val data = intent.data
|
||||||
|
if (data == null) {
|
||||||
|
returnToSettings()
|
||||||
|
} else {
|
||||||
|
handleResult(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun returnToSettings() {
|
internal fun returnToSettings() {
|
||||||
|
|||||||
@@ -2,69 +2,64 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TrackLoginActivity : BaseOAuthLoginActivity() {
|
class TrackLoginActivity : BaseOAuthLoginActivity() {
|
||||||
|
|
||||||
override fun handleResult(data: Uri?) {
|
override fun handleResult(uri: Uri) {
|
||||||
when (data?.host) {
|
val data = when {
|
||||||
"anilist-auth" -> handleAnilist(data)
|
!uri.encodedQuery.isNullOrBlank() -> uri.encodedQuery
|
||||||
"bangumi-auth" -> handleBangumi(data)
|
!uri.encodedFragment.isNullOrBlank() -> uri.encodedFragment
|
||||||
"myanimelist-auth" -> handleMyAnimeList(data)
|
else -> null
|
||||||
"shikimori-auth" -> handleShikimori(data)
|
}
|
||||||
|
?.split("&")
|
||||||
|
?.filter { it.isNotBlank() }
|
||||||
|
?.associate {
|
||||||
|
val parts = it.split("=", limit = 2).map<String, String>(Uri::decode)
|
||||||
|
parts[0] to parts.getOrNull(1)
|
||||||
|
}
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
when (uri.host) {
|
||||||
|
"anilist-auth" -> handleAniList(data["access_token"])
|
||||||
|
"bangumi-auth" -> handleBangumi(data["code"])
|
||||||
|
"myanimelist-auth" -> handleMyAnimeList(data["code"])
|
||||||
|
"shikimori-auth" -> handleShikimori(data["code"])
|
||||||
|
}
|
||||||
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAnilist(data: Uri) {
|
private suspend fun handleAniList(accessToken: String?) {
|
||||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
if (accessToken != null) {
|
||||||
val matchResult = regex.find(data.fragment.toString())
|
trackerManager.aniList.login(accessToken)
|
||||||
if (matchResult?.groups?.get(1) != null) {
|
|
||||||
lifecycleScope.launchIO {
|
|
||||||
trackerManager.aniList.login(matchResult.groups[1]!!.value)
|
|
||||||
returnToSettings()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
trackerManager.aniList.logout()
|
trackerManager.aniList.logout()
|
||||||
returnToSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBangumi(data: Uri) {
|
private suspend fun handleBangumi(code: String?) {
|
||||||
val code = data.getQueryParameter("code")
|
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
lifecycleScope.launchIO {
|
trackerManager.bangumi.login(code)
|
||||||
trackerManager.bangumi.login(code)
|
|
||||||
returnToSettings()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
trackerManager.bangumi.logout()
|
trackerManager.bangumi.logout()
|
||||||
returnToSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMyAnimeList(data: Uri) {
|
private suspend fun handleMyAnimeList(code: String?) {
|
||||||
val code = data.getQueryParameter("code")
|
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
lifecycleScope.launchIO {
|
trackerManager.myAnimeList.login(code)
|
||||||
trackerManager.myAnimeList.login(code)
|
|
||||||
returnToSettings()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
trackerManager.myAnimeList.logout()
|
trackerManager.myAnimeList.logout()
|
||||||
returnToSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShikimori(data: Uri) {
|
private suspend fun handleShikimori(code: String?) {
|
||||||
val code = data.getQueryParameter("code")
|
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
lifecycleScope.launchIO {
|
trackerManager.shikimori.login(code)
|
||||||
trackerManager.shikimori.login(code)
|
|
||||||
returnToSettings()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
trackerManager.shikimori.logout()
|
trackerManager.shikimori.logout()
|
||||||
returnToSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.util.fastFilter
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
@@ -27,11 +28,16 @@ import kotlinx.coroutines.flow.catch
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.common.preference.TriState
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@@ -40,9 +46,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||||
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
@@ -57,6 +65,7 @@ class UpdatesScreenModel(
|
|||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
|
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
@@ -75,19 +84,35 @@ class UpdatesScreenModel(
|
|||||||
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
||||||
|
|
||||||
combine(
|
combine(
|
||||||
getUpdates.subscribe(limit).distinctUntilChanged(),
|
// needed for SQL filters (unread, started, bookmarked, etc)
|
||||||
|
getUpdatesItemPreferenceFlow()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flatMapLatest {
|
||||||
|
getUpdates.subscribe(
|
||||||
|
limit,
|
||||||
|
unread = it.filterUnread.toBooleanOrNull(),
|
||||||
|
started = it.filterStarted.toBooleanOrNull(),
|
||||||
|
bookmarked = it.filterBookmarked.toBooleanOrNull(),
|
||||||
|
hideExcludedScanlators = it.filterExcludedScanlators,
|
||||||
|
).distinctUntilChanged()
|
||||||
|
},
|
||||||
downloadCache.changes,
|
downloadCache.changes,
|
||||||
downloadManager.queueState,
|
downloadManager.queueState,
|
||||||
) { updates, _, _ -> updates }
|
// needed for Kotlin filters (downloaded)
|
||||||
.catch {
|
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
|
||||||
logcat(LogPriority.ERROR, it)
|
old.filterDownloaded == new.filterDownloaded
|
||||||
_events.send(Event.InternalError)
|
},
|
||||||
}
|
) { updates, _, _, itemPreferences ->
|
||||||
.collectLatest { updates ->
|
updates
|
||||||
|
.toUpdateItems()
|
||||||
|
.applyFilters(itemPreferences)
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
|
.collectLatest { updateItems ->
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
items = updates.toUpdateItems(),
|
items = updateItems,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,9 +123,43 @@ class UpdatesScreenModel(
|
|||||||
.catch { logcat(LogPriority.ERROR, it) }
|
.catch { logcat(LogPriority.ERROR, it) }
|
||||||
.collect(this@UpdatesScreenModel::updateDownloadState)
|
.collect(this@UpdatesScreenModel::updateDownloadState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdatesItemPreferenceFlow()
|
||||||
|
.map { prefs ->
|
||||||
|
listOf(
|
||||||
|
prefs.filterUnread,
|
||||||
|
prefs.filterDownloaded,
|
||||||
|
prefs.filterStarted,
|
||||||
|
prefs.filterBookmarked,
|
||||||
|
)
|
||||||
|
.any { it != TriState.DISABLED }
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
mutableState.update { state ->
|
||||||
|
state.copy(hasActiveFilters = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(screenModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<UpdatesWithRelations>.toUpdateItems(): PersistentList<UpdatesItem> {
|
private fun List<UpdatesItem>.applyFilters(
|
||||||
|
preferences: ItemPreferences,
|
||||||
|
): List<UpdatesItem> {
|
||||||
|
val filterDownloaded = preferences.filterDownloaded
|
||||||
|
|
||||||
|
val filterFnDownloaded: (UpdatesItem) -> Boolean = {
|
||||||
|
applyFilter(filterDownloaded) {
|
||||||
|
it.downloadStateProvider() == Download.State.DOWNLOADED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fastFilter {
|
||||||
|
filterFnDownloaded(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
||||||
return this
|
return this
|
||||||
.map { update ->
|
.map { update ->
|
||||||
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
||||||
@@ -123,7 +182,6 @@ class UpdatesScreenModel(
|
|||||||
selected = update.chapterId in selectedChapterIds,
|
selected = update.chapterId in selectedChapterIds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toPersistentList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibrary(): Boolean {
|
fun updateLibrary(): Boolean {
|
||||||
@@ -267,7 +325,6 @@ class UpdatesScreenModel(
|
|||||||
fun toggleSelection(
|
fun toggleSelection(
|
||||||
item: UpdatesItem,
|
item: UpdatesItem,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
userSelected: Boolean = false,
|
|
||||||
fromLongPress: Boolean = false,
|
fromLongPress: Boolean = false,
|
||||||
) {
|
) {
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
@@ -282,7 +339,7 @@ class UpdatesScreenModel(
|
|||||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||||
|
|
||||||
if (selected && userSelected && fromLongPress) {
|
if (selected && fromLongPress) {
|
||||||
if (firstSelection) {
|
if (firstSelection) {
|
||||||
selectedPositions[0] = selectedIndex
|
selectedPositions[0] = selectedIndex
|
||||||
selectedPositions[1] = selectedIndex
|
selectedPositions[1] = selectedIndex
|
||||||
@@ -308,7 +365,7 @@ class UpdatesScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (userSelected && !fromLongPress) {
|
} else if (!fromLongPress) {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
if (selectedIndex == selectedPositions[0]) {
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
selectedPositions[0] = indexOfFirst { it.selected }
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
@@ -361,9 +418,41 @@ class UpdatesScreenModel(
|
|||||||
libraryPreferences.newUpdatesCount().set(0)
|
libraryPreferences.newUpdatesCount().set(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUpdatesItemPreferenceFlow(): Flow<ItemPreferences> {
|
||||||
|
return combine(
|
||||||
|
updatesPreferences.filterDownloaded().changes(),
|
||||||
|
updatesPreferences.filterUnread().changes(),
|
||||||
|
updatesPreferences.filterStarted().changes(),
|
||||||
|
updatesPreferences.filterBookmarked().changes(),
|
||||||
|
updatesPreferences.filterExcludedScanlators().changes(),
|
||||||
|
) { downloaded, unread, started, bookmarked, excludedScanlators ->
|
||||||
|
ItemPreferences(
|
||||||
|
filterDownloaded = downloaded,
|
||||||
|
filterUnread = unread,
|
||||||
|
filterStarted = started,
|
||||||
|
filterBookmarked = bookmarked,
|
||||||
|
filterExcludedScanlators = excludedScanlators,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showFilterDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class ItemPreferences(
|
||||||
|
val filterDownloaded: TriState,
|
||||||
|
val filterUnread: TriState,
|
||||||
|
val filterStarted: TriState,
|
||||||
|
val filterBookmarked: TriState,
|
||||||
|
val filterExcludedScanlators: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
val hasActiveFilters: Boolean = false,
|
||||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||||
val dialog: Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
) {
|
) {
|
||||||
@@ -387,6 +476,7 @@ class UpdatesScreenModel(
|
|||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
||||||
|
data object FilterSheet : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
@@ -395,6 +485,14 @@ class UpdatesScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TriState.toBooleanOrNull(): Boolean? {
|
||||||
|
return when (this) {
|
||||||
|
TriState.DISABLED -> null
|
||||||
|
TriState.ENABLED_IS -> true
|
||||||
|
TriState.ENABLED_NOT -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class UpdatesItem(
|
data class UpdatesItem(
|
||||||
val update: UpdatesWithRelations,
|
val update: UpdatesWithRelations,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.updates
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import tachiyomi.core.common.preference.Preference
|
||||||
|
import tachiyomi.core.common.preference.TriState
|
||||||
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class UpdatesSettingsScreenModel(
|
||||||
|
val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||||
|
) : ScreenModel {
|
||||||
|
|
||||||
|
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
|
||||||
|
preference(updatesPreferences).getAndSet {
|
||||||
|
it.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
|||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
import eu.kanade.presentation.updates.UpdateScreen
|
import eu.kanade.presentation.updates.UpdateScreen
|
||||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||||
|
import eu.kanade.presentation.updates.UpdatesFilterDialog
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||||
@@ -54,6 +55,7 @@ data object UpdatesTab : Tab {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||||
|
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
UpdateScreen(
|
UpdateScreen(
|
||||||
@@ -74,6 +76,8 @@ data object UpdatesTab : Tab {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||||
|
onFilterClicked = screenModel::showFilterDialog,
|
||||||
|
hasActiveFilters = state.hasActiveFilters,
|
||||||
)
|
)
|
||||||
|
|
||||||
val onDismissDialog = { screenModel.setDialog(null) }
|
val onDismissDialog = { screenModel.setDialog(null) }
|
||||||
@@ -84,6 +88,12 @@ data object UpdatesTab : Tab {
|
|||||||
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is UpdatesScreenModel.Dialog.FilterSheet -> {
|
||||||
|
UpdatesFilterDialog(
|
||||||
|
onDismissRequest = onDismissDialog,
|
||||||
|
screenModel = settingsScreenModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package androidx.preference
|
|||||||
/**
|
/**
|
||||||
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||||
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||||
return onBindEditTextListener
|
return onBindEditTextListener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.test
|
package eu.kanade.test
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
@@ -20,8 +19,7 @@ data class DummyTracker(
|
|||||||
override val supportsPrivateTracking: Boolean = false,
|
override val supportsPrivateTracking: Boolean = false,
|
||||||
override val isLoggedIn: Boolean = false,
|
override val isLoggedIn: Boolean = false,
|
||||||
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
||||||
val valLogoColor: Int = Color.rgb(18, 25, 35),
|
val valLogo: Int = R.drawable.brand_anilist,
|
||||||
val valLogo: Int = R.drawable.ic_tracker_anilist,
|
|
||||||
val valStatuses: List<Long> = (1L..6L).toList(),
|
val valStatuses: List<Long> = (1L..6L).toList(),
|
||||||
val valReadingStatus: Long = 1L,
|
val valReadingStatus: Long = 1L,
|
||||||
val valRereadingStatus: Long = 1L,
|
val valRereadingStatus: Long = 1L,
|
||||||
@@ -34,8 +32,6 @@ data class DummyTracker(
|
|||||||
override val client: OkHttpClient
|
override val client: OkHttpClient
|
||||||
get() = TODO("Not yet implemented")
|
get() = TODO("Not yet implemented")
|
||||||
|
|
||||||
override fun getLogoColor(): Int = valLogoColor
|
|
||||||
|
|
||||||
override fun getLogo(): Int = valLogo
|
override fun getLogo(): Int = valLogo
|
||||||
|
|
||||||
override fun getStatusList(): List<Long> = valStatuses
|
override fun getStatusList(): List<Long> = valStatuses
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ class ShellInterface : IShellInterface.Stub() {
|
|||||||
.invoke(pmInterface)
|
.invoke(pmInterface)
|
||||||
|
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||||
|
val installFlags = this::class.java.getField("installFlags")
|
||||||
|
installFlags.set(
|
||||||
|
this,
|
||||||
|
installFlags.getInt(this) or REPLACE_EXISTING_INSTALL_FLAG,
|
||||||
|
)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
|
setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
|
||||||
}
|
}
|
||||||
@@ -105,26 +111,27 @@ class ShellInterface : IShellInterface.Stub() {
|
|||||||
.getMethod("openSession", Int::class.java)
|
.getMethod("openSession", Int::class.java)
|
||||||
.invoke(packageInstaller, sessionId)
|
.invoke(packageInstaller, sessionId)
|
||||||
|
|
||||||
(
|
session::class.java.getMethod(
|
||||||
session::class.java.getMethod(
|
"openWrite",
|
||||||
"openWrite",
|
String::class.java,
|
||||||
String::class.java,
|
Long::class.java,
|
||||||
Long::class.java,
|
Long::class.java,
|
||||||
Long::class.java,
|
)
|
||||||
).invoke(session, "extension", 0L, apk.length) as ParcelFileDescriptor
|
.invoke(session, "extension", 0L, apk.length)
|
||||||
).let { fd ->
|
.let { it as ParcelFileDescriptor }
|
||||||
val revocable = Class.forName("android.os.SystemProperties")
|
.let { fd ->
|
||||||
.getMethod("getBoolean", String::class.java, Boolean::class.java)
|
val revocable = Class.forName("android.os.SystemProperties")
|
||||||
.invoke(null, "fw.revocable_fd", false) as Boolean
|
.getMethod("getBoolean", String::class.java, Boolean::class.java)
|
||||||
|
.invoke(null, "fw.revocable_fd", false) as Boolean
|
||||||
|
|
||||||
if (revocable) {
|
if (revocable) {
|
||||||
ParcelFileDescriptor.AutoCloseOutputStream(fd)
|
ParcelFileDescriptor.AutoCloseOutputStream(fd)
|
||||||
} else {
|
} else {
|
||||||
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
|
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
|
||||||
.getConstructor(ParcelFileDescriptor::class.java)
|
.getConstructor(ParcelFileDescriptor::class.java)
|
||||||
.newInstance(fd) as OutputStream
|
.newInstance(fd) as OutputStream
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.use { output ->
|
.use { output ->
|
||||||
apk.createInputStream().use { input -> input.copyTo(output) }
|
apk.createInputStream().use { input -> input.copyTo(output) }
|
||||||
}
|
}
|
||||||
@@ -174,3 +181,7 @@ class ShellInterface : IShellInterface.Stub() {
|
|||||||
return shellContext.createPackageContext("com.android.shell", 0)
|
return shellContext.createPackageContext("com.android.shell", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constant hidden from the SDK
|
||||||
|
// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/core/java/android/content/pm/PackageManager.java;l=1682-1689
|
||||||
|
private const val REPLACE_EXISTING_INSTALL_FLAG = 0x00000002
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -61,7 +62,6 @@ import tachiyomi.domain.source.service.SourceManager
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.Pill
|
import tachiyomi.presentation.core.components.Pill
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
@@ -143,7 +143,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
SmallExtendedFloatingActionButton(
|
||||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||||
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -330,13 +330,13 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
|
private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) {
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
val updatedSources = action(state.sources)
|
val updatedSources = action(state.sources)
|
||||||
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
||||||
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
||||||
}
|
}
|
||||||
if (save) saveSources()
|
saveSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSources() {
|
private fun initSources() {
|
||||||
@@ -368,7 +368,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
|||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
updateSources(save = false) { sources }
|
mutableState.update { state ->
|
||||||
|
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSelection(id: Long) {
|
fun toggleSelection(id: Long) {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
|
|||||||
}
|
}
|
||||||
val selectedFlags = sourcePreference.migrationFlags().get()
|
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(
|
State(
|
||||||
current = current,
|
current = current,
|
||||||
target = target,
|
target = target,
|
||||||
applicableFlags = applicableFlags,
|
applicableFlags = applicableFlags,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ abstract class BaseSmartSearchEngine<T>(
|
|||||||
val eligibleManga = supervisorScope {
|
val eligibleManga = supervisorScope {
|
||||||
queries.map { query ->
|
queries.map { query ->
|
||||||
async(Dispatchers.Default) {
|
async(Dispatchers.Default) {
|
||||||
val builtQuery = if (extraSearchParams != null) {
|
val builtQuery = if (!extraSearchParams.isNullOrBlank()) {
|
||||||
"$query ${extraSearchParams.trim()}"
|
"$query $extraSearchParams"
|
||||||
} else {
|
} else {
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package mihon.feature.migration.list.search
|
package mihon.feature.migration.list.search
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import mihon.domain.manga.model.toDomainManga
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -23,6 +22,6 @@ class SmartSourceSearchEngine(extraSearchParams: String?) : BaseSmartSearchEngin
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun makeSearchAction(source: CatalogueSource): SearchAction<SManga> = { query ->
|
private fun makeSearchAction(source: CatalogueSource): SearchAction<SManga> = { query ->
|
||||||
source.getSearchManga(1, query, FilterList()).mangas
|
source.getSearchManga(1, query, source.getFilterList()).mangas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ fun CalenderHeader(
|
|||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
IconButton(onClick = onPreviousClick) {
|
IconButton(onClick = onPreviousClick) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
||||||
}
|
}
|
||||||
IconButton(onClick = onNextClick) {
|
IconButton(onClick = onNextClick) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
|
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h192v192H0z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#1f2631"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m63.6,43.6 l-36.8,105h28.6l6.2,-18.2h31.2l6.1,18.1h28.5L90.7,43.6zM68.2,107.1 L77.1,78.1 86.9,107.1z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#fefefe"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M127,148.5h32.5q6.3,0 6.4,-6.3v-14q-0.1,-6.4 -6.4,-6.5h-37.6V50q-0.1,-6.3 -6.4,-6.4h-14q-6.3,0.1 -6.4,6.4v7.8c-3.2,-9.8 31.8,90.7 32,90.7"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#02a9ff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0.03,0.03l192.01,0l0,192.01L0.03,192.03z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0.03,0.03l191.95,0l0,191.95L0.03,191.97z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#2d2e2f"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M129.89,-36.09a3.42,3.42 0,1 1,5.7 3.87L87.4,36.59A31.9,31.9 0,0 1,100.22 46.16L157.23,46.16a17.09,17.09 0,0 1,17.09 17.09l0,93.98a17.09,17.09 0,0 1,-17.09 17.09L67.98,174.32L43.88,192.54a5.7,5.7 0,0 1,-8.54 -7.01l5.35,-11.22L-6.81,174.32a25.06,25.06 0,0 1,-25.06 -25.06L-31.87,63.25a17.09,17.09 0,0 1,17.09 -17.09L50.66,46.16a31.9,31.9 0,0 1,10.77 -8.71L-6.13,-31.76a3.42,3.42 0,0 1,4.9 -4.78L68.49,34.94a31.9,31.9 0,0 1,11.96 -0.34zM40.47,187.99l18.06,-13.67L46.96,174.32zM-6.81,55.28a7.97,7.97 0,0 0,-7.97 7.97l0,86.01a7.97,7.97 0,0 0,7.97 7.97l164.04,0a7.97,7.97 0,0 0,7.97 -7.97L165.2,63.25a7.97,7.97 0,0 0,-7.97 -7.97z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="71.22"
|
||||||
|
android:startY="57.21"
|
||||||
|
android:endX="71.22"
|
||||||
|
android:endY="154.89"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF3599"/>
|
||||||
|
<item android:offset="1" android:color="#FFFF77AB"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M157.23,55.28a7.97,7.97 0,0 1,7.97 7.97l0,86.01a7.97,7.97 0,0 1,-7.97 7.97L81.08,157.23L40.47,187.99l14.7,-30.76L-6.81,157.23a7.97,7.97 0,0 1,-7.97 -7.97L-14.78,63.25a7.97,7.97 0,0 1,7.97 -7.97z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M93.44,92.3a2.28,2.28 0,0 1,1.94 3.42l-18.23,30.76a2.28,2.28 0,0 1,-3.87 0l-18.23,-30.76a2.28,2.28 0,0 1,1.94 -3.42zM145.84,119.07a2.28,2.28 0,0 1,0 4.56l-36.17,0a2.28,2.28 0,0 1,0 -4.56zM40.75,118.78a2.28,2.28 0,0 1,0 4.56L4.58,123.34a2.28,2.28 0,0 1,0 -4.56zM75.21,120.89 L89.45,96.85L60.97,96.85zM145.84,111.09a2.28,2.28 0,0 1,0 4.56l-36.17,0a2.28,2.28 0,0 1,0 -4.56zM40.75,110.81a2.28,2.28 0,0 1,0 4.56L4.58,115.37a2.28,2.28 0,0 1,0 -4.56zM144.98,78.51a2.28,2.28 0,1 1,1.71 4.21L118.9,94.01l27.8,11.28a2.28,2.28 0,1 1,-1.71 4.21l-33.04,-13.39a2.28,2.28 0,0 1,0 -4.21zM2.48,79.48a2.28,2.28 0,0 1,2.96 -1.25l33.04,13.39a2.28,2.28 0,0 1,0 4.21l-33.04,13.39a2.28,2.28 0,0 1,-1.71 -4.21l27.85,-11.28 -27.85,-11.28a2.28,2.28 0,0 1,-1.25 -2.96"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="75.21"
|
||||||
|
android:startY="78.17"
|
||||||
|
android:endX="75.21"
|
||||||
|
android:endY="129.75"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF3599"/>
|
||||||
|
<item android:offset="1" android:color="#FFFF77AB"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h192v192H0z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#49c593"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M46.5,43.8c44.3,0 44.3,0 48,3.1 0.8,1 0.8,1 0.8,2.4h1.4l0.5,-1.2c0.9,-1.6 0.9,-1.6 4.3,-4.4h44v90.8l-30,0.2h-7.2l-6.8,0.1c-2.5,0.5 -3.2,1.3 -4.8,3.1h-1.4l-1.2,-1.3c-2,-1.9 -3.3,-1.8 -6,-1.8h-8.6l-10.1,-0.2 -22.9,-0.1z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#fefefe"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M39.6,54.8h6.9v79.7l30,-0.2h14c2.3,0.2 3.2,0.7 4.8,2.3v1.3h1.4l0.6,-1.3c0.8,-1.4 0.8,-1.4 2.1,-2.1h46.1V54.8h6.9c1.7,1.7 1.6,2.3 1.6,4.8v80.3c-0.2,1.5 -0.2,1.5 -1.6,2.8 -1.8,0.3 -1.8,0.3 -4.2,0.3h-37.8c-3.3,0.6 -5,2.6 -7.2,5 -2.5,2.4 -4.3,3 -7.8,3.3 -3.3,0 -5.3,-1.5 -7.7,-3.7L85,144c-2,-1 -3.4,-0.7 -5.5,-0.8h-4.1l-17,-0.2 -14.6,-0.1h-2.3c-1.9,-0.2 -1.9,-0.2 -3.2,-1.6 -0.3,-1.5 -0.3,-1.5 -0.3,-3.5V57.7c0.3,-1.6 0.3,-1.6 1.6,-3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#424d72"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M46.5,43.8h43.3v0.6H47.2V129l29.6,-0.2h13.7c2.3,0.2 3.2,0.7 4.8,2.3v1.3h1.4l0.6,-1.3c0.8,-1.4 0.8,-1.4 2.1,-2.1h45.3V44.4h-43.3v-0.6h44v90.7l-30,0.2h-7.2l-6.8,0.1c-2.4,0.5 -3.1,1.3 -4.8,3.1h-1.4l-1.1,-1.3c-2,-1.9 -3.4,-1.8 -6.1,-1.8h-8.6l-10,-0.2 -22.9,-0.1z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#e2e6ec"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M57.5,52h11v19.3h-11z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#59d1f7"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M103.6,119.4h31.6v2.7h-31.6zM56.8,119.4h31.6v2.7L56.8,122.1zM103.6,111.1h31.6v2.8h-31.6zM56.8,111.1h31.6v2.8L56.8,113.9zM103.6,102.9h31.6v2.7h-31.6zM56.8,102.9h31.6v2.7L56.8,105.6zM103.6,94.6h31.6v2.8h-31.6zM56.8,94.6h31.6v2.8L56.8,97.4zM103.6,86.4h31.6L135.2,89h-31.6zM56.8,86.4h31.6L88.4,89L56.8,89zM103.6,78.1h31.6L135.2,81h-31.6zM56.8,78.1h31.6L88.4,81L56.8,81zM103.6,69.9h31.6v2.7h-31.6z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#e5e8f0"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M103.6,61.6h31.6v2.8h-31.6zM103.6,53.4h31.6L135.2,56h-31.6z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#e4e7ef"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M74.7,69.9h13.7v2.7H74.7z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#e5e8f0"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M74.7,61.6h13.7v2.8L74.7,64.4zM74.7,53.4h13.7L88.4,56L74.7,56z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#e5e8ef"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h192v192H0z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#312631"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m113,51.3 l-2.2,0.2 -4.2,0.5 -1.5,0.2a59,59 0,0 0,-11.3 4L92,57v12.5l-0.2,13.7a10,10 0,0 1,-5.4 7.1,6 6,0 0,1 -3.5,0.8L80,91a189,189 0,0 1,-14.5 -5.5l-4.8,-1.9 -5.5,-1.8c-0.2,0 -1,0.8 -1.8,1.5l-2,1.7a199,199 0,0 0,-19 19.5,5 5,0 0,0 -1.5,4.3c0,0.8 1.3,2.5 2.2,2.9 1,0.5 2.4,0.6 3.3,0.3l2.5,-1.5a117,117 0,0 1,21.4 -11.9l2.2,-1 3,-1c2,0 4,1.5 4.4,3.2l0.2,0.8c0.3,0.3 -1.5,4.2 -2.2,4.9 -0.2,0.1 -0.3,0.4 -1.3,2.2l-0.7,1a64,64 0,0 0,-3.7 7.2l-0.8,1.7 -0.4,0.8 -0.9,2a98,98 0,0 0,-3.3 8.3c-1.4,4 -1.4,3.8 -1.4,5.2q0,2 1.6,3.3 0.8,0.8 2.3,0.8c1.9,0 2.8,-0.4 4.1,-2.2l2.6,-3.4a64,64 0,0 1,5.5 -6.6c1.5,-1.9 8.7,-9 10.5,-10.6a109,109 0,0 1,22.5 -16A125,125 0,0 1,138 86.6c3,-0.6 3.6,-0.6 4.6,0.6 0.7,0.7 0.6,2.4 -0.1,3.2a8,8 0,0 1,-3.7 1.5,76 76,0 0,0 -25.1,12 42,42 0,0 0,-6.8 5A66,66 0,0 0,89.5 132a51,51 0,0 0,-2.8 9.9,47 47,0 0,0 2.7,24c0.7,1.8 1.2,2.3 2.4,3 1.9,0.9 4.2,0.1 5.5,-2 3.6,-5.8 7.4,-9 15.5,-13l3,-1.4 0.7,-0.4 0.6,-0.3 1.8,-0.7 2.4,-1c1.4,-0.6 8.6,-4.3 9.7,-5l3.2,-2 3.6,-2.4 2.1,-1.4c2.6,-1.9 8,-7 10,-9.3l1,-1.3a44,44 0,0 0,5.8 -9.2,44 44,0 0,0 4.8,-21c0,-5 0,-5.9 -0.8,-9.5a48,48 0,0 0,-7.8 -18,47 47,0 0,0 -26,-18.1l-1.4,-0.3 -3,-0.7a69,69 0,0 0,-9.5 -0.7m-17.5,83.2a28,28 0,0 0,16.9 11.7l1,0.2 1.1,0.2q1.7,0.2 0.2,0.7a89,89 0,0 0,-13.2 7.3,34 34,0 0,0 -7.2,7.4c-0.7,1 -0.7,0.9 -1.7,-3.2a54,54 0,0 1,-1 -15,42 42,0 0,1 3.1,-10.3z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#f75239"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M72.3,22.7c-1.5,0.3 -2.4,1.2 -3.9,3.5L66.6,30a30,30 0,0 0,-2.2 14.7c0.1,0.9 0.1,1 -1.1,1.6a50,50 0,0 0,-6.2 4.1l-1.6,1.3q-0.3,0.2 -1.6,-0.5a31,31 0,0 0,-16.7 -2.8c-4,0.6 -4.7,0.9 -5.6,2q-1.1,1.1 -1,2.7c0,1 0.6,2.8 1.2,3.4l1,1.2 0.6,1 1.5,1.7c3.5,4 8.3,8.5 12.5,11.3 4,2.6 5.5,3.3 12,5.8l8.4,3.3 1.7,0.6 3.6,1.4 2,0.8c2,0.9 5.3,2 6.4,2.2q1.3,0.4 2.3,0a4,4 0,0 0,2.7 -3,613 613,0 0,0 -0.4,-34.6 43,43 0,0 0,-1.4 -6.2L83,37c-0.5,-1.3 -4,-8.2 -5.2,-10.4 -1.8,-3.2 -3.3,-4.2 -5.5,-3.9m1,7.2c3,5.8 4.4,8.6 4.8,9.7 0.4,1.3 0.4,1.4 0,1.5h-0.7l-1,0.3 -1.7,0.4 -1.3,0.3 -0.6,0.2q-3,1 -3,0.8a15,15 0,0 1,0 -4.3l0.2,-1.9c0.3,-2.1 2.4,-7.4 3,-7.4zM44,54c2.4,0.3 6.3,1.5 7,2q0,0.4 -1.2,2l-1.6,2 -1.4,2 -1,1.6 -1.1,-1a76,76 0,0 1,-7.6 -8.6h1.3a26,26 0,0 1,5.6 0"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#f75239"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h192v192H0z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#005ed3"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M-0.01,-0.01l191.98,0l0,191.98L-0.01,191.98z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M223.63,135.63a88,88 70.86,0 1,-99.89 87.18l-31.63,-31.63 0.89,-5.95 -28.19,-28.22 22.1,-21.38 -22.14,-21.38 31.63,-5.29 -4.26,-28.91 28.6,12.72 14.88,-25.58 22.17,22.34 21.31,-9.49 43.69,43.69q0.83,5.84 0.83,11.89"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00459f"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M215.63,127.63a88,88 49.79,0 1,-99.86 87.21l-31.63,-31.63 0.89,-5.98 -28.26,-28.22 22.14,-21.38 -22.14,-21.38 31.63,-5.29 -4.26,-28.91 28.6,12.72L127.63,59.22l22.17,22.34 21.31,-9.49 43.72,43.72q0.79,5.84 0.79,11.86"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00459f"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M184,96a88,88 65.09,0 1,-99.86 87.21l-31.63,-31.63 0.89,-5.98 -28.26,-28.22 22.14,-21.38L25.19,74.62l31.63,-5.29 -4.26,-28.91 28.6,12.72L96,27.59l22.17,22.34 21.31,-9.49 43.72,43.72q0.79,5.84 0.79,11.86"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00459f"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m96,37.8 l12.75,21.93 24.27,-10.79 -3.61,24.65 26.47,4.43 -18.6,17.98 18.6,17.98 -26.47,4.47 3.61,24.61 -24.27,-10.79 -12.75,21.93 -12.75,-21.93 -24.27,10.79 3.61,-24.65 -26.47,-4.43 18.6,-17.98 -18.6,-17.98 26.47,-4.47 -3.61,-24.61 24.27,10.79z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#ff0335"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m155.88,113.98 l-26.47,4.47 3.61,24.61 -24.27,-10.79 -12.75,21.93L96,37.8l12.75,21.93 24.27,-10.79 -2.34,15.81 -1.27,8.83 20.21,3.37 6.26,1.07 -4.61,4.47 -13.99,13.51 3.92,3.78z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#c2001b"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m96,164.41 l-14.88,-25.54 -28.6,12.72 4.26,-28.91 -31.63,-5.29 22.14,-21.38L25.19,74.62l31.63,-5.29 -4.26,-28.91 28.6,12.72L96,27.59l14.88,25.58 28.6,-12.72 -4.26,28.91 31.63,5.29 -22.14,21.38 22.14,21.38 -31.63,5.29 4.26,28.91 -28.6,-12.72zM85.34,125.67 L96,143.92 106.66,125.7 126.56,134.53 123.57,114.18 144.95,110.57 129.86,96 144.98,81.43 123.57,77.82 126.56,57.47 106.62,66.33 96,48.08 85.34,66.3 65.44,57.47 68.43,77.82 47.05,81.43 62.14,96 47.02,110.57 68.43,114.18 65.44,134.53z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#ffdf47"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m146.63,97.86 l-1.92,-1.86 8.63,-8.35 13.51,-13.03 -19.18,-3.2 -12.44,-2.06 0.96,-6.6 3.3,-22.34 -28.6,12.72L96,27.56l0,20.52l10.66,18.22 19.9,-8.83 -1.38,9.14 -0.86,5.98 -0.76,5.22 0.86,0.17 20.52,3.44 -15.09,14.58 5.84,5.64 9.28,8.94 -21.42,3.61 2.99,20.35 -19.94,-8.87 -10.62,18.25l0,20.52l14.88,-25.58 28.6,12.72 -4.26,-28.91 31.63,-5.29z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#fec000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m116.97,54.77 l-5.65,33.08 25.29,30.59l-25.1,7.46l-17.68,-24.55l7.88,27.46l-24.31,7.22L59.05,72.01l24.31,-7.22l7.13,24.87l3.67,-28.08z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.6"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:strokeColor="#000"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="192"
|
||||||
|
android:viewportHeight="192">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h192v192H0z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#92a0ad"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M44.7,19h102.6A25.6,25.6 0,0 1,173 44.7v102.6a25.6,25.6 0,0 1,-25.7 25.7H44.7A25.6,25.6 0,0 1,19 147.3V44.7A25.6,25.6 0,0 1,44.7 19"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#cbd6e8"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M164.4,143.2q0,3.7 -0.5,7.5a22,22 0,0 1,-2.1 7q-1.5,3 -4.3,5.2a12,12 0,0 1,-7.4 2q-5.5,0 -8.5,-3.4a20,20 0,0 1,-3.8 -8.6,118 118,0 0,1 -2.3,-16.7l-1.6,-33.8a105,105 0,0 0,-2.8 -16.7,567 567,0 0,1 -8.5,26.3l-3.8,9.6a127,127 0,0 0,-5.2 17.8,175 175,0 0,1 -3,12.2q-0.7,2.2 -1.8,4.7 -0.8,2.4 -1.9,4.5a10,10 0,0 1,-2.2 3.1q-2.8,2.6 -6.7,3.3a26,26 0,0 1,-7 1,11 11,0 0,1 -9,-4 27,27 0,0 1,-4.9 -8.8,56 56,0 0,1 -1.9,-7.3l-1.5,-7.5q-2.3,-9.9 -5.4,-19.5 -3,-9.7 -5.6,-19.5 -3.5,7.8 -6.3,15.6a107,107 0,0 0,-4.7 28.6q0,6 -1.6,12.2 -1.2,4.5 -4,7 -2.6,2.6 -7.5,2.6a14,14 0,0 1,-12.2 -6.6q-1.7,-3 -2.4,-6.6 -0.5,-4 -0.5,-7.7 0,-8.2 2,-17.4 2.3,-9.5 5.1,-18.8l6.1,-18.7q3.4,-9.3 5.8,-17.2 2.3,-7.9 3.3,-15.5 1.2,-7.7 3,-15.5A26,26 0,0 1,55 31.4a21,21 0,0 1,5.6 -4q3,-1.5 6.8,-1.6 2.6,0 5.4,0.9a15,15 0,0 1,8.7 5.9q1.5,2.3 1.4,5.4 0,2.3 -0.4,4.5 -0.3,2.1 -0.3,4.4a271,271 0,0 0,10.6 74.9q7.2,-16.4 12,-33.5a1608,1608 0,0 0,12 -44.6q1.3,-5.1 3.7,-10 2.3,-4.5 5.6,-7.4 3.4,-3 8.7,-3a29,29 0,0 1,12.2 2.8q2.5,1.5 4.2,4 1.8,2.6 1.7,6.3 0,2.8 -0.3,5.6l-0.2,5.4a219,219 0,0 0,6.3 52q2.4,10.8 4,21.9 1.7,10.8 1.7,21.9"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#ff8c15"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
</vector>
|
||||||