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_type-argument-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
|
||||
- label: I have written a short but informative title.
|
||||
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
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.19.2"
|
||||
Example: "0.19.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -95,7 +95,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.19.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
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
|
||||
@@ -26,20 +26,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Dependency Review
|
||||
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
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- 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
|
||||
run: ./gradlew spotlessCheck
|
||||
@@ -48,16 +48,24 @@ jobs:
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
|
||||
- name: Run unit tests
|
||||
id: unit_tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload test report
|
||||
if: steps.unit_tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: test-report-${{ github.sha }}
|
||||
path: app/build/reports/tests/testReleaseUnitTest
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release
|
||||
|
||||
@@ -29,16 +29,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mihon
|
||||
path: |
|
||||
@@ -83,16 +83,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: mihon-foss
|
||||
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
@@ -143,12 +143,12 @@ jobs:
|
||||
mihon-foss
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||
body: |
|
||||
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving!
|
||||
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you're upgrading from an earlier version. Consider [becoming a patron](https://www.patreon.com/mihon/membership) to help keep Mihon improving!
|
||||
|
||||
<!-->
|
||||
|
||||
|
||||
@@ -12,6 +12,57 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [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
|
||||
### 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))
|
||||
@@ -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))
|
||||
- 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.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
|
||||
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 15
|
||||
versionName = "0.19.2"
|
||||
versionCode = 17
|
||||
versionName = "0.19.4"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -159,12 +159,14 @@ kotlin {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xannotation-default-target=param-property",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -270,6 +272,7 @@ dependencies {
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.bundles.markdown)
|
||||
implementation(libs.materialKolor)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
@@ -57,6 +57,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -157,6 +158,7 @@ class DomainModule : InjektModule {
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetBookmarkedChaptersByMangaId(get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
|
||||
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||
|
||||
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||
onClick = onCreate,
|
||||
|
||||
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -195,7 +196,7 @@ fun AppBarActions(
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(it.title)
|
||||
@@ -220,7 +221,7 @@ fun AppBarActions(
|
||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||
@@ -349,7 +350,7 @@ fun SearchToolbar(
|
||||
// Don't show search action
|
||||
} else if (searchQuery == null) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_search))
|
||||
@@ -369,7 +370,7 @@ fun SearchToolbar(
|
||||
}
|
||||
} else if (searchQuery.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_reset))
|
||||
|
||||
@@ -22,7 +22,7 @@ fun relativeDateText(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.takeIf { dateEpochMillis > 0L },
|
||||
.takeIf { dateEpochMillis != 0L },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun DownloadDropdownMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
offset: DpOffset? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (offset != null) {
|
||||
DropdownMenu(
|
||||
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
private fun DownloadDropdownMenuItems(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDownloadClicked: (DownloadAction) -> Unit,
|
||||
) {
|
||||
@@ -59,6 +58,7 @@ private fun ColumnScope.DownloadDropdownMenuItems(
|
||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||
)
|
||||
|
||||
options.map { (downloadAction, string) ->
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package eu.kanade.presentation.manga
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -27,9 +24,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -71,7 +70,6 @@ import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.VerticalFastScroller
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -124,7 +122,7 @@ fun MangaScreen(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -255,7 +253,7 @@ private fun MangaScreenSmallImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -330,25 +328,23 @@ private fun MangaScreenSmallImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
@@ -499,7 +495,7 @@ fun MangaScreenLargeImpl(
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
@@ -572,27 +568,25 @@ fun MangaScreenLargeImpl(
|
||||
val isFABVisible = remember(chapters) {
|
||||
chapters.fastAny { !it.chapter.read } && !isAnySelected
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isFABVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val isReading = remember(state.chapters) {
|
||||
state.chapters.fastAny { it.chapter.read }
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isReading) MR.strings.action_resume else MR.strings.action_start,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
expanded = chapterListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isFABVisible,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
PullRefresh(
|
||||
@@ -745,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
) {
|
||||
items(
|
||||
@@ -793,14 +787,14 @@ private fun LazyListScope.sharedChapterItems(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
onChapterSelected(item, !item.selected, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ enum class DownloadAction {
|
||||
NEXT_10_CHAPTERS,
|
||||
NEXT_25_CHAPTERS,
|
||||
UNREAD_CHAPTERS,
|
||||
BOOKMARKED_CHAPTERS,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
|
||||
@@ -91,10 +91,10 @@ fun MangaBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
@@ -252,10 +252,10 @@ fun LibraryBottomActionMenu(
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
var resetJob by remember { mutableStateOf<Job?>(null) }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
|
||||
@@ -567,44 +567,47 @@ private fun ColumnScope.MangaContentInfo(
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
@Composable
|
||||
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
|
||||
markdownAnnotator(
|
||||
annotate = { content, child ->
|
||||
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
|
||||
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
|
||||
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
|
||||
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
|
||||
?.getUnescapedTextInNode(content)
|
||||
?: return@markdownAnnotator false
|
||||
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
|
||||
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
|
||||
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
|
||||
?.getUnescapedTextInNode(content).orEmpty()
|
||||
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
withLink(LinkAnnotation.Url(url = url)) {
|
||||
pushStyle(linkStyle)
|
||||
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
|
||||
append(altText)
|
||||
pop()
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
|
||||
append(content.substring(child.startOffset, child.endOffset))
|
||||
return@markdownAnnotator true
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
false
|
||||
},
|
||||
config = markdownAnnotatorConfig(
|
||||
eolAsNewLine = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaSummary(
|
||||
|
||||
@@ -225,7 +225,6 @@ object AboutScreen : Screen() {
|
||||
is GetApplicationRelease.Result.OsTooOld -> {
|
||||
context.toast(MR.strings.update_check_eol)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
val libraries by produceLibraries(R.raw.aboutlibraries)
|
||||
LibrariesContainer(
|
||||
libraries = libraries,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
|
||||
@@ -28,7 +28,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
pref = screenModel.preferences.customBrightness(),
|
||||
)
|
||||
|
||||
/**
|
||||
/*
|
||||
* Sets the brightness of the screen. Range is [-75, 100].
|
||||
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
||||
* From 1 to 100 it sets that value as brightness.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package eu.kanade.presentation.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
|
||||
isAmoled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = getThemeColorScheme(appTheme, isAmoled),
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = remember(appTheme, isDark, isAmoled) {
|
||||
getThemeColorScheme(
|
||||
context = context,
|
||||
appTheme = appTheme,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getThemeColorScheme(
|
||||
context: Context,
|
||||
appTheme: AppTheme,
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
MonetColorScheme(context)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
isDark = isDark,
|
||||
isAmoled = isAmoled,
|
||||
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
|
||||
private val surfaceContainerHigh = Color(0xFF131313)
|
||||
private val surfaceContainerHighest = Color(0xFF1B1B1B)
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
fun getColorScheme(
|
||||
isDark: Boolean,
|
||||
isAmoled: Boolean,
|
||||
overrideDarkSurfaceContainers: Boolean,
|
||||
): ColorScheme {
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
val amoledScheme = darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
if (!overrideDarkSurfaceContainers) return amoledScheme
|
||||
|
||||
return amoledScheme.copy(
|
||||
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
|
||||
surfaceContainerLowest = surfaceContainer,
|
||||
surfaceContainerLow = surfaceContainer,
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.UiModeManager
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MaterialDynamicColors
|
||||
import com.google.android.material.color.utilities.QuantizerCelebi
|
||||
import com.google.android.material.color.utilities.SchemeContent
|
||||
import com.google.android.material.color.utilities.Score
|
||||
import com.materialkolor.PaletteStyle
|
||||
import com.materialkolor.dynamiccolor.ColorSpec
|
||||
import com.materialkolor.ktx.DynamicScheme
|
||||
import com.materialkolor.toColorScheme
|
||||
|
||||
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
?.primaryColor
|
||||
?.toArgb()
|
||||
if (seed != null) {
|
||||
MonetCompatColorScheme(context, seed)
|
||||
MonetCompatColorScheme(Color(seed))
|
||||
} else {
|
||||
TachiyomiColorScheme
|
||||
}
|
||||
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme
|
||||
get() = monet.lightScheme
|
||||
|
||||
companion object {
|
||||
@Suppress("Unused")
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val bitmapPixels = IntArray(width * height)
|
||||
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
|
||||
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
|
||||
.takeIf { it != 0 } // Don't take fallback color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
|
||||
override val darkScheme = dynamicDarkColorScheme(context)
|
||||
}
|
||||
|
||||
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
|
||||
|
||||
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
|
||||
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
|
||||
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
|
||||
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
|
||||
|
||||
companion object {
|
||||
private fun Int.toComposeColor(): Color = Color(this)
|
||||
|
||||
@SuppressLint("PrivateResource", "RestrictedApi")
|
||||
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
|
||||
val scheme = SchemeContent(
|
||||
Hct.fromInt(seed),
|
||||
dark,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
)
|
||||
val dynamicColors = MaterialDynamicColors()
|
||||
return ColorScheme(
|
||||
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
|
||||
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
|
||||
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
|
||||
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
|
||||
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
|
||||
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
|
||||
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
|
||||
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
|
||||
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
|
||||
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
|
||||
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
|
||||
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
|
||||
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
|
||||
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
|
||||
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
|
||||
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
|
||||
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
|
||||
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
|
||||
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
|
||||
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
|
||||
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
|
||||
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
|
||||
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
|
||||
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
|
||||
scrim = Color.Black,
|
||||
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
|
||||
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
|
||||
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
|
||||
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
|
||||
return DynamicScheme(
|
||||
seedColor = seed,
|
||||
isDark = dark,
|
||||
specVersion = ColorSpec.SpecVersion.SPEC_2025,
|
||||
style = PaletteStyle.Expressive,
|
||||
)
|
||||
.toColorScheme(isAmoled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.track
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.platform.toClipEntry
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
@@ -73,6 +75,7 @@ import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -240,7 +243,7 @@ private fun SearchResultItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
|
||||
@@ -248,6 +251,7 @@ private fun SearchResultItem(
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||
var dropDownMenuExpanded by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -295,7 +299,13 @@ private fun SearchResultItem(
|
||||
expanded = dropDownMenuExpanded,
|
||||
onCollapseMenu = { dropDownMenuExpanded = false },
|
||||
onCopyName = {
|
||||
clipboardManager.setText(AnnotatedString(trackSearch.title))
|
||||
scope.launch {
|
||||
val clipEntry = ClipData.newPlainText(
|
||||
trackSearch.title,
|
||||
trackSearch.title,
|
||||
).toClipEntry()
|
||||
clipboard.setClipEntry(clipEntry)
|
||||
}
|
||||
},
|
||||
onOpenInBrowser = {
|
||||
val url = trackSearch.tracking_url
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -30,18 +26,13 @@ fun TrackLogoIcon(
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(tracker.getLogo()),
|
||||
contentDescription = tracker.name,
|
||||
)
|
||||
}
|
||||
.clip(MaterialTheme.shapes.medium),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package eu.kanade.presentation.track.components
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.test.DummyTracker
|
||||
|
||||
@@ -13,8 +11,6 @@ internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker>
|
||||
DummyTracker(
|
||||
id = 1L,
|
||||
name = "Dummy Tracker",
|
||||
valLogoColor = Color.rgb(18, 25, 35),
|
||||
valLogo = R.drawable.ic_tracker_anilist,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package eu.kanade.presentation.updates
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun UpdatesFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(screenModel = screenModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterSheet(
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = filterDownloaded,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
)
|
||||
|
||||
val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
)
|
||||
|
||||
val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
)
|
||||
|
||||
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
|
||||
|
||||
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState()
|
||||
|
||||
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { toggleScanlatorFilter() }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_filter_excluded_scanlators),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = filterExcludedScanlators,
|
||||
onCheckedChange = { toggleScanlatorFilter() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@@ -37,6 +40,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -54,8 +58,10 @@ fun UpdateScreen(
|
||||
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
|
||||
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
|
||||
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onOpenChapter: (UpdatesItem) -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode) {
|
||||
onSelectAll(false)
|
||||
@@ -66,6 +72,8 @@ fun UpdateScreen(
|
||||
UpdatesAppBar(
|
||||
onCalendarClicked = { onCalendarClicked() },
|
||||
onUpdateLibrary = { onUpdateLibrary() },
|
||||
onFilterClicked = { onFilterClicked() },
|
||||
hasFilters = hasActiveFilters,
|
||||
actionModeCounter = state.selected.size,
|
||||
onSelectAll = { onSelectAll(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
@@ -133,6 +141,8 @@ fun UpdateScreen(
|
||||
private fun UpdatesAppBar(
|
||||
onCalendarClicked: () -> Unit,
|
||||
onUpdateLibrary: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
hasFilters: Boolean,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
@@ -147,6 +157,12 @@ private fun UpdatesAppBar(
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onFilterClicked,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_view_upcoming),
|
||||
icon = Icons.Outlined.CalendarMonth,
|
||||
|
||||
@@ -69,7 +69,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
|
||||
internal fun LazyListScope.updatesUiItems(
|
||||
uiModels: List<UpdatesUiModel>,
|
||||
selectionMode: Boolean,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
onClickUpdate: (UpdatesItem) -> Unit,
|
||||
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
|
||||
@@ -111,11 +111,11 @@ internal fun LazyListScope.updatesUiItems(
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
|
||||
onUpdateSelected(updatesItem, !updatesItem.selected, true)
|
||||
},
|
||||
onClick = {
|
||||
when {
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
|
||||
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
|
||||
else -> onClickUpdate(updatesItem)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
import tachiyomi.i18n.MR
|
||||
import java.net.UnknownHostException
|
||||
|
||||
context(Context)
|
||||
context(context: Context)
|
||||
val Throwable.formattedMessage: String
|
||||
get() {
|
||||
when (this) {
|
||||
is HttpException -> return stringResource(MR.strings.exception_http, code)
|
||||
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
|
||||
is UnknownHostException -> {
|
||||
return if (!isOnline()) {
|
||||
stringResource(MR.strings.exception_offline)
|
||||
return if (!context.isOnline()) {
|
||||
context.stringResource(MR.strings.exception_offline)
|
||||
} else {
|
||||
stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
context.stringResource(MR.strings.exception_unknown_host, message ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
is NoResultsException -> return stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
|
||||
is NoResultsException -> return context.stringResource(MR.strings.no_results_found)
|
||||
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
|
||||
}
|
||||
return when (val className = this::class.simpleName) {
|
||||
"Exception", "IOException" -> message ?: className
|
||||
|
||||
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
// https://issuetracker.google.com/352584409
|
||||
context(LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
context(itemScope: LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
|
||||
import android.os.Message
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -28,7 +29,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.stack.mutableStateStackOf
|
||||
import com.kevinnzou.web.AccompanistWebChromeClient
|
||||
@@ -36,12 +39,13 @@ import com.kevinnzou.web.AccompanistWebViewClient
|
||||
import com.kevinnzou.web.LoadingState
|
||||
import com.kevinnzou.web.WebContent
|
||||
import com.kevinnzou.web.WebView
|
||||
import com.kevinnzou.web.WebViewNavigator
|
||||
import com.kevinnzou.web.WebViewState
|
||||
import com.kevinnzou.web.rememberWebViewNavigator
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getHtml
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -50,13 +54,13 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
class WebViewWindow(webContent: WebContent) {
|
||||
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
|
||||
var state by mutableStateOf(WebViewState(webContent))
|
||||
var popupMessage: Message? = null
|
||||
private set
|
||||
var webView: WebView? = null
|
||||
|
||||
constructor(popupMessage: Message) : this(WebContent.NavigatorOnly) {
|
||||
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
|
||||
this.popupMessage = popupMessage
|
||||
}
|
||||
}
|
||||
@@ -72,27 +76,20 @@ fun WebViewScreenContent(
|
||||
headers: Map<String, String> = emptyMap(),
|
||||
onUrlChange: (String) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val windowStack = remember {
|
||||
mutableStateStackOf(
|
||||
WebViewWindow(
|
||||
WebContent.Url(url = url, additionalHttpHeaders = headers),
|
||||
WebViewNavigator(coroutineScope),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val currentWindow = windowStack.lastItemOrNull!!
|
||||
val navigator = currentWindow.navigator
|
||||
|
||||
val popState: (() -> Unit) = remember {
|
||||
{
|
||||
if (windowStack.size == 1) {
|
||||
onNavigateUp()
|
||||
} else {
|
||||
windowStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val navigator = rememberWebViewNavigator()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -133,21 +130,20 @@ fun WebViewScreenContent(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
request?.let {
|
||||
// Don't attempt to open blobs as webpages
|
||||
if (it.url.toString().startsWith("blob:http")) {
|
||||
return false
|
||||
}
|
||||
val url = request?.url?.toString() ?: return false
|
||||
|
||||
// Ignore intents urls
|
||||
if (it.url.toString().startsWith("intent://")) {
|
||||
// Ignore intents urls
|
||||
if (url.startsWith("intent://")) return true
|
||||
|
||||
// Only open valid web urls
|
||||
if (url.startsWith("http") || url.startsWith("https")) {
|
||||
if (url != view?.url) {
|
||||
view?.loadUrl(url, headers)
|
||||
return true
|
||||
}
|
||||
|
||||
// Continue with request, but with custom headers
|
||||
view?.loadUrl(it.url.toString(), headers)
|
||||
}
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +158,7 @@ fun WebViewScreenContent(
|
||||
): Boolean {
|
||||
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
|
||||
if (isUserGesture) {
|
||||
windowStack.push(WebViewWindow(resultMsg))
|
||||
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -177,6 +173,18 @@ fun WebViewScreenContent(
|
||||
return webView
|
||||
}
|
||||
|
||||
val popState = remember<() -> Unit> {
|
||||
{
|
||||
if (windowStack.size == 1) {
|
||||
onNavigateUp()
|
||||
} else {
|
||||
windowStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(windowStack.size > 1, popState)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Box {
|
||||
@@ -184,7 +192,7 @@ fun WebViewScreenContent(
|
||||
AppBar(
|
||||
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||
subtitle = currentUrl,
|
||||
navigateUp = popState,
|
||||
navigateUp = onNavigateUp,
|
||||
navigationIcon = Icons.Outlined.Close,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
@@ -225,7 +233,18 @@ fun WebViewScreenContent(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = { onClearCookies(currentUrl) },
|
||||
),
|
||||
),
|
||||
).builder().apply {
|
||||
if (windowStack.size > 1) {
|
||||
add(
|
||||
0,
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_webview_close_tab),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
|
||||
onClick = popState,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.build(),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -254,7 +273,7 @@ fun WebViewScreenContent(
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
is LoadingState.Loading -> LinearProgressIndicator(
|
||||
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
|
||||
progress = { loadingState.progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
@@ -298,7 +317,7 @@ fun WebViewScreenContent(
|
||||
// The composable is being disposed but the WebView object is not.
|
||||
// When the WebView element is recomposed, we will want the WebView to resume from its state
|
||||
// before it was unmounted, we won't want it to reset back to its original target.
|
||||
window.state = WebViewState(WebContent.NavigatorOnly)
|
||||
window.state.content = WebContent.NavigatorOnly
|
||||
}
|
||||
},
|
||||
client = webClient,
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
@@ -157,8 +158,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
// Updates widget update
|
||||
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
|
||||
|
||||
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
val minLogPriority = when {
|
||||
networkPreferences.verboseLogging().get() -> LogPriority.VERBOSE
|
||||
BuildConfig.DEBUG -> LogPriority.DEBUG
|
||||
else -> LogPriority.INFO
|
||||
}
|
||||
LogcatLogger.install()
|
||||
LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority)
|
||||
}
|
||||
|
||||
initializeMigrator()
|
||||
@@ -196,6 +203,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
add(MangaKeyer())
|
||||
}
|
||||
|
||||
memoryCache(
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context)
|
||||
.build(),
|
||||
)
|
||||
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
|
||||
@@ -418,10 +418,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
*/
|
||||
private const val KEY_CATEGORY = "category"
|
||||
|
||||
fun cancelAllWorks(context: Context) {
|
||||
context.workManager.cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(
|
||||
context: Context,
|
||||
prefInterval: Int? = null,
|
||||
@@ -435,16 +431,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
val networkRequest = NetworkRequest.Builder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -24,9 +23,6 @@ interface Tracker {
|
||||
|
||||
val supportsPrivateTracking: Boolean
|
||||
|
||||
@ColorInt
|
||||
fun getLogoColor(): Int
|
||||
|
||||
@DrawableRes
|
||||
fun getLogo(): Int
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
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 getLogoColor() = Color.rgb(18, 25, 35)
|
||||
override fun getLogo() = R.drawable.brand_anilist
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
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 {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
||||
val query = $$"""
|
||||
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
@@ -78,14 +78,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
val query = $$"""
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|
||||
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|id: $listId, progress: $progress, status: $status, private: $private,
|
||||
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|
||||
|) {
|
||||
|id
|
||||
|status
|
||||
@@ -114,9 +114,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun deleteLibManga(track: DomainTrack) {
|
||||
withIOContext {
|
||||
val query = """
|
||||
|mutation DeleteManga(${'$'}listId: Int) {
|
||||
|DeleteMediaListEntry(id: ${'$'}listId) {
|
||||
val query = $$"""
|
||||
|mutation DeleteManga($listId: Int) {
|
||||
|DeleteMediaListEntry(id: $listId) {
|
||||
|deleted
|
||||
|}
|
||||
|}
|
||||
@@ -135,10 +135,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query Search(${'$'}query: String) {
|
||||
val query = $$"""
|
||||
|query Search($query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|id
|
||||
|staff {
|
||||
|edges {
|
||||
@@ -197,10 +197,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
val query = $$"""
|
||||
|query ($id: Int!, $manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|
||||
|id
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -79,9 +78,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_bangumi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(240, 145, 153)
|
||||
override fun getLogo() = R.drawable.brand_bangumi
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
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
|
||||
// replaces the stringified ID in certain queries.
|
||||
// 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)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.kavita
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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()
|
||||
|
||||
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
||||
|
||||
override fun getLogoColor() = Color.rgb(74, 198, 148)
|
||||
override fun getLogo(): Int = R.drawable.brand_kavita
|
||||
|
||||
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.jwtToken = token.toString()
|
||||
authentication.jwtToken = token
|
||||
}
|
||||
authentications = oauth
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_kitsu
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
override fun getLogo() = R.drawable.brand_kitsu
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.komga
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_komga
|
||||
|
||||
override fun getLogoColor() = Color.rgb(51, 37, 50)
|
||||
override fun getLogo() = R.drawable.brand_komga
|
||||
|
||||
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.mangaupdates
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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) }
|
||||
|
||||
override fun getLogo(): Int = R.drawable.ic_manga_updates
|
||||
|
||||
override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
|
||||
override fun getLogo(): Int = R.drawable.brand_mangaupdates
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -41,9 +39,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_mal
|
||||
|
||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||
override fun getLogo() = R.drawable.brand_myanimelist
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
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.MALSearchResult
|
||||
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.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
@@ -78,15 +75,15 @@ class MyAnimeListApi(
|
||||
// MAL API throws a 400 when the query is over 64 characters...
|
||||
.appendQueryParameter("q", query.take(64))
|
||||
.appendQueryParameter("nsfw", "true")
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALSearchResult>()
|
||||
.data
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.filter { !it.publishing_type.contains("novel") }
|
||||
.filter { !(it.node.mediaType.contains("novel")) }
|
||||
.map { parseSearchItem(it.node) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,29 +92,13 @@ class MyAnimeListApi(
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendPath(id.toString())
|
||||
.appendQueryParameter(
|
||||
"fields",
|
||||
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
||||
)
|
||||
.appendQueryParameter("fields", SEARCH_FIELDS)
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<MALManga>()
|
||||
.let {
|
||||
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 ?: ""
|
||||
}
|
||||
}
|
||||
.let { parseSearchItem(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,8 +162,7 @@ class MyAnimeListApi(
|
||||
|
||||
val matches = myListSearchResult.data
|
||||
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||
.map { async { getMangaDetails(it.node.id) } }
|
||||
.awaitAll()
|
||||
.map { parseSearchItem(it.node) }
|
||||
|
||||
// Check next page if there's more
|
||||
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 {
|
||||
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())
|
||||
if (offset > 0) {
|
||||
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 {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
@@ -236,7 +238,7 @@ class MyAnimeListApi(
|
||||
return try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(epochTime)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -247,6 +249,9 @@ class MyAnimeListApi(
|
||||
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 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 var codeVerifier: String = ""
|
||||
|
||||
@@ -18,8 +18,26 @@ data class MALManga(
|
||||
val mediaType: String,
|
||||
@SerialName("start_date")
|
||||
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
|
||||
data class MALMangaCovers(
|
||||
val large: String = "",
|
||||
|
||||
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class MALSearchResult(
|
||||
val data: List<MALSearchResultNode>,
|
||||
val paging: MALSearchPaging,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultNode(
|
||||
val node: MALSearchResultItem,
|
||||
val node: MALManga,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MALSearchResultItem(
|
||||
val id: Int,
|
||||
data class MALSearchPaging(
|
||||
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
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -98,9 +96,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
override fun getLogo() = R.drawable.brand_shikimori
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
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) }
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO
|
||||
override fun getLogo() = R.drawable.brand_suwayomi
|
||||
|
||||
companion object {
|
||||
const val UNREAD = 1L
|
||||
const val READING = 2L
|
||||
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)
|
||||
@@ -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 {
|
||||
@@ -105,4 +105,9 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
||||
|
||||
private fun String.getMangaId(): Long =
|
||||
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
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.addAll
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@@ -29,19 +32,22 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
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 baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
||||
|
||||
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
|
||||
|
||||
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
|
||||
val query = """
|
||||
|query GetManga(${'$'}mangaId: Int!) {
|
||||
| manga(id: ${'$'}mangaId) {
|
||||
val query = $$"""
|
||||
|query GetManga($mangaId: Int!) {
|
||||
| manga(id: $mangaId) {
|
||||
| ...MangaFragment
|
||||
| }
|
||||
|}
|
||||
|
|
||||
|$MangaFragment
|
||||
|$$MangaFragment
|
||||
""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
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 chaptersQuery = """
|
||||
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
|
||||
// TODO: Include a filter on the chapter number here
|
||||
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
|
||||
val chaptersQuery = $$"""
|
||||
|query GetMangaUnreadChapters($mangaId: Int!) {
|
||||
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
|
||||
| nodes {
|
||||
| id
|
||||
| chapterNumber
|
||||
@@ -110,18 +118,29 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.data
|
||||
.entry
|
||||
.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 = """
|
||||
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
|
||||
| chapters {
|
||||
| id
|
||||
| }
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
val markQuery = if (deleteDownloadsOnServer) {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
| deleteDownloadedChapters(input: {ids: $chapters}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
$$"""
|
||||
|mutation MarkChaptersRead($chapters: [Int!]!) {
|
||||
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
}
|
||||
val markPayload = buildJsonObject {
|
||||
put("query", markQuery)
|
||||
putJsonObject("variables") {
|
||||
@@ -140,12 +159,10 @@ class SuwayomiApi(private val trackId: Long) {
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
val trackQuery = """
|
||||
|mutation TrackManga(${'$'}mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
|
||||
| trackRecords {
|
||||
| lastChapterRead
|
||||
| }
|
||||
val trackQuery = $$"""
|
||||
|mutation TrackManga($mangaId: Int!) {
|
||||
| trackProgress(input: {mangaId: $mangaId}) {
|
||||
| __typename
|
||||
| }
|
||||
|}
|
||||
""".trimMargin()
|
||||
|
||||
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
@@ -46,6 +47,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory {
|
||||
LibraryPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpdatesPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
ReaderPreferences(get())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
@SuppressLint("RequestInstallPackagesPolicy")
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
try {
|
||||
shellInterface?.install(
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
|
||||
)
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
|
||||
shellInterface?.install(it)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
continueQueue(InstallStep.Error)
|
||||
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
override fun onDestroy() {
|
||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||
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)
|
||||
logcat { "ShizukuInstaller destroy" }
|
||||
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) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
|
||||
@@ -1,66 +1,48 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
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.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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*
|
||||
* @param context The application 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>>()
|
||||
internal class ExtensionInstaller(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
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 httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
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]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
val step = MutableStateFlow(InstallStep.Pending)
|
||||
activeSteps[downloadId] = step
|
||||
|
||||
// Register the receiver after removing (and unregistering) the previous download
|
||||
downloadReceiver.register()
|
||||
val job = scope.launch {
|
||||
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()
|
||||
val request = DownloadManager.Request(downloadUri)
|
||||
.setTitle(extension.name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Failed to download extension")
|
||||
}
|
||||
response.body.byteStream().use { input ->
|
||||
tmpFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
|
||||
downloadsStateFlows[id] = downloadStateFlow
|
||||
|
||||
// Poll download status
|
||||
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
|
||||
// Map to our model
|
||||
when (downloadStatus) {
|
||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||
else -> null
|
||||
step.value = InstallStep.Installing
|
||||
installApk(downloadId, tmpFile)
|
||||
} catch (e: Exception) {
|
||||
if (e is InterruptedException) {
|
||||
// Canceled
|
||||
} else {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
step.value = InstallStep.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
|
||||
emit(it)
|
||||
// Stop when the application is installed or errors
|
||||
!it.isCompleted()
|
||||
}.onCompletion {
|
||||
// Always notify on main thread
|
||||
withUIContext {
|
||||
// Always remove the download when unsubscribed
|
||||
deleteDownload(pkgName)
|
||||
activeJobs[extension.pkgName] = job
|
||||
|
||||
return step.asStateFlow()
|
||||
.onCompletion {
|
||||
activeJobs.remove(extension.pkgName)
|
||||
activeSteps.remove(downloadId)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
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 {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
|
||||
updateInstallStep(downloadId, InstallStep.Installed)
|
||||
} else {
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
|
||||
updateInstallStep(downloadId, InstallStep.Error)
|
||||
}
|
||||
|
||||
tempFile.delete()
|
||||
}
|
||||
else -> {
|
||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
val intent = ExtensionInstallService.getIntent(
|
||||
context,
|
||||
downloadId,
|
||||
tempFile.getUriCompat(context),
|
||||
installer,
|
||||
)
|
||||
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.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
Installer.cancelInstallQueue(context, downloadId)
|
||||
activeJobs.remove(pkgName)?.cancel()
|
||||
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param step New install step.
|
||||
*/
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
downloadsStateFlows[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))
|
||||
}
|
||||
}
|
||||
}
|
||||
activeSteps[downloadId]?.let { it.value = step }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
||||
state.map { it.searchQuery }
|
||||
.distinctUntilChanged()
|
||||
.debounce(SEARCH_DEBOUNCE_MILLIS)
|
||||
.map { searchQueryPredicate(it ?: "") },
|
||||
currentDownloads,
|
||||
getExtensions.subscribe(),
|
||||
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
val searchQuery = query ?: ""
|
||||
|
||||
val itemsGroups: ItemGroups = mutableMapOf()
|
||||
|
||||
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))
|
||||
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
buildMap {
|
||||
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||
}
|
||||
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 ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
items = it,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
|
||||
.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?) {
|
||||
mutableState.update {
|
||||
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 {
|
||||
sealed interface Header {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -49,6 +50,10 @@ fun extensionsTab(
|
||||
),
|
||||
),
|
||||
content = { contentPadding, _ ->
|
||||
BackHandler(enabled = state.searchQuery != null) {
|
||||
extensionsScreenModel.search(null)
|
||||
}
|
||||
|
||||
ExtensionScreen(
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
|
||||
@@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (state.selectionMode) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
val selection = state.selection
|
||||
screenModel.clearSelection()
|
||||
navigator.push(MigrationConfigScreen(selection))
|
||||
},
|
||||
expanded = lazyListState.shouldExpandFAB(),
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.selectionMode,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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.LocalUriHandler
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@@ -34,7 +37,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
@@ -73,13 +75,15 @@ data class MigrateSourceSearchScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
)
|
||||
}
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.filters.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.download
|
||||
|
||||
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.layout.Box
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Pill
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
)
|
||||
}
|
||||
val isRunning by screenModel.isDownloaderRunning.collectAsState()
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
val id = if (isRunning) {
|
||||
MR.strings.action_pause
|
||||
} else {
|
||||
MR.strings.action_resume
|
||||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = {
|
||||
val icon = if (isRunning) {
|
||||
Icons.Outlined.Pause
|
||||
} else {
|
||||
Icons.Filled.PlayArrow
|
||||
}
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
if (isRunning) {
|
||||
screenModel.pauseDownloads()
|
||||
} else {
|
||||
screenModel.startDownloads()
|
||||
}
|
||||
},
|
||||
expanded = fabExpanded,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = downloadList.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (downloadList.isEmpty()) {
|
||||
|
||||
@@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.source.local.LocalSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
private const val LOCAL_SOURCE_ID_ALIAS = "local"
|
||||
|
||||
data class LibraryItem(
|
||||
val libraryManga: LibraryManga,
|
||||
val downloadCount: Long = -1,
|
||||
@@ -23,9 +26,17 @@ data class LibraryItem(
|
||||
* @return true if the manga matches the query, false otherwise.
|
||||
*/
|
||||
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)) {
|
||||
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) ||
|
||||
(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.SetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||
@@ -73,6 +74,7 @@ class LibraryScreenModel(
|
||||
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
|
||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
@@ -234,9 +236,7 @@ class LibraryScreenModel(
|
||||
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val mangaTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
||||
.orEmpty()
|
||||
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||
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
|
||||
*/
|
||||
fun performDownloadAction(action: DownloadAction) {
|
||||
val mangas = state.value.selectedManga
|
||||
val amount = when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
||||
DownloadAction.UNREAD_CHAPTERS -> null
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
private fun downloadNextChapters(amount: Int?) {
|
||||
val mangas = state.value.selectedManga
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
@@ -59,12 +58,10 @@ import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import mihon.feature.migration.config.MigrationConfigScreen
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class MangaScreen(
|
||||
@@ -325,12 +322,7 @@ class MangaScreen(
|
||||
try {
|
||||
getMangaUrl(manga_, source_)?.let { url ->
|
||||
val intent = url.toUri().toShareIntent(context, type = "text/plain")
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
|
||||
@@ -650,6 +650,13 @@ class MangaScreenModel(
|
||||
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(
|
||||
chapters: List<Chapter>,
|
||||
startNow: Boolean,
|
||||
@@ -712,6 +719,7 @@ class MangaScreenModel(
|
||||
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
|
||||
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
|
||||
}
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
startDownload(chaptersToDownload, false)
|
||||
@@ -953,7 +961,6 @@ class MangaScreenModel(
|
||||
fun toggleSelection(
|
||||
item: ChapterList.Item,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
updateSuccessState { successState ->
|
||||
@@ -968,7 +975,7 @@ class MangaScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.id, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -994,7 +1001,7 @@ class MangaScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
|
||||
@@ -344,7 +344,9 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.flushReadTimer()
|
||||
lifecycleScope.launchNonCancellable {
|
||||
viewModel.updateHistory()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -576,7 +578,7 @@ class ReaderActivity : BaseActivity() {
|
||||
private fun shareChapter() {
|
||||
assistUrl?.let {
|
||||
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,
|
||||
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) {
|
||||
@@ -777,7 +779,8 @@ class ReaderActivity : BaseActivity() {
|
||||
* Updates viewer inset depending on fullscreen reader preferences.
|
||||
*/
|
||||
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)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
|
||||
@@ -832,7 +835,7 @@ class ReaderActivity : BaseActivity() {
|
||||
|
||||
private val grayBackgroundColor = Color.rgb(0x20, 0x21, 0x25)
|
||||
|
||||
/**
|
||||
/*
|
||||
* Initializes the reader subscriptions.
|
||||
*/
|
||||
init {
|
||||
|
||||
@@ -346,7 +346,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
viewModelScope.launchIO {
|
||||
logcat { "Loading ${chapter.chapter.url}" }
|
||||
|
||||
flushReadTimer()
|
||||
updateHistory()
|
||||
restartReadTimer()
|
||||
|
||||
try {
|
||||
@@ -501,7 +501,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
* if setting is enabled and [currentChapter] is queued for 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))
|
||||
}
|
||||
}
|
||||
@@ -585,26 +585,20 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
chapterReadStartTime = Instant.now().toEpochMilli()
|
||||
}
|
||||
|
||||
fun flushReadTimer() {
|
||||
getCurrentChapter()?.let {
|
||||
viewModelScope.launchNonCancellable {
|
||||
updateHistory(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the chapter last read history if incognito mode isn't on.
|
||||
*/
|
||||
private suspend fun updateHistory(readerChapter: ReaderChapter) {
|
||||
if (incognitoMode) return
|
||||
suspend fun updateHistory() {
|
||||
getCurrentChapter()?.let { readerChapter ->
|
||||
if (incognitoMode) return@let
|
||||
|
||||
val chapterId = readerChapter.chapter.id!!
|
||||
val endTime = Date()
|
||||
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
||||
val chapterId = readerChapter.chapter.id!!
|
||||
val endTime = Date()
|
||||
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
|
||||
|
||||
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
||||
chapterReadStartTime = null
|
||||
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
|
||||
chapterReadStartTime = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,7 +649,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
viewModelScope.launchNonCancellable {
|
||||
updateChapter.await(
|
||||
ChapterUpdate(
|
||||
id = chapter.id!!.toLong(),
|
||||
id = chapter.id!!,
|
||||
bookmark = bookmarked,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -51,15 +51,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
// Add previous chapter pages and transition
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// 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) {
|
||||
@@ -101,14 +94,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Resets double-page splits, else insert pages get misplaced
|
||||
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
|
||||
@@ -123,7 +109,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
|
||||
// Will skip insert page otherwise
|
||||
if (insertPageLastPage != null) {
|
||||
viewer.moveToPage(insertPageLastPage!!)
|
||||
viewer.moveToPage(insertPageLastPage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +180,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
|
||||
fun cleanupPageSplit() {
|
||||
val insertPages = items.filterIsInstance(InsertPage::class.java)
|
||||
val insertPages = items.filterIsInstance<InsertPage>()
|
||||
items.removeAll(insertPages)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -43,14 +43,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
if (chapters.prevChapter != null) {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// 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) {
|
||||
@@ -70,14 +63,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||
}
|
||||
|
||||
if (chapters.nextChapter != null) {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
updateItems(newItems)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
recycler?.onManualScroll()
|
||||
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.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
@@ -35,10 +34,6 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private var firstVisibleItemPosition = 0
|
||||
private var lastVisibleItemPosition = 0
|
||||
private var currentScale = DEFAULT_RATE
|
||||
|
||||
private var isScrolling = false
|
||||
private var hasTappedWhileScrolling = false
|
||||
|
||||
var zoomOutDisabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -57,6 +52,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
var tapListener: ((MotionEvent) -> Unit)? = null
|
||||
var longTapListener: ((MotionEvent) -> Boolean)? = null
|
||||
|
||||
private var isManuallyScrolling = false
|
||||
private var tapDuringManualScroll = false
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
halfWidth = MeasureSpec.getSize(widthSpec) / 2
|
||||
halfHeight = MeasureSpec.getSize(heightSpec) / 2
|
||||
@@ -67,11 +65,11 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
hasTappedWhileScrolling = isScrolling
|
||||
tapDuringManualScroll = isManuallyScrolling
|
||||
}
|
||||
|
||||
detector.onTouchEvent(e)
|
||||
return super.onTouchEvent(e)
|
||||
}
|
||||
@@ -91,7 +89,10 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
val totalItemCount = layoutManager?.itemCount ?: 0
|
||||
atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
|
||||
atFirstPosition = firstVisibleItemPosition == 0
|
||||
isScrolling = state != SCROLL_STATE_IDLE
|
||||
|
||||
if (state == SCROLL_STATE_IDLE) {
|
||||
isManuallyScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPositionX(positionX: Float): Float {
|
||||
@@ -222,10 +223,14 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onManualScroll() {
|
||||
isManuallyScrolling = true
|
||||
}
|
||||
|
||||
inner class GestureListener : GestureDetectorWithLongTap.Listener() {
|
||||
|
||||
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
|
||||
if (!hasTappedWhileScrolling) {
|
||||
if (!tapDuringManualScroll) {
|
||||
tapListener?.invoke(ev)
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -14,7 +14,7 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
||||
|
||||
internal val trackerManager: TrackerManager by injectLazy()
|
||||
|
||||
abstract fun handleResult(data: Uri?)
|
||||
abstract fun handleResult(uri: Uri)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -23,7 +23,12 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
|
||||
LoadingScreen()
|
||||
}
|
||||
|
||||
handleResult(intent.data)
|
||||
val data = intent.data
|
||||
if (data == null) {
|
||||
returnToSettings()
|
||||
} else {
|
||||
handleResult(data)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun returnToSettings() {
|
||||
|
||||
@@ -2,69 +2,64 @@ package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TrackLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
override fun handleResult(data: Uri?) {
|
||||
when (data?.host) {
|
||||
"anilist-auth" -> handleAnilist(data)
|
||||
"bangumi-auth" -> handleBangumi(data)
|
||||
"myanimelist-auth" -> handleMyAnimeList(data)
|
||||
"shikimori-auth" -> handleShikimori(data)
|
||||
override fun handleResult(uri: Uri) {
|
||||
val data = when {
|
||||
!uri.encodedQuery.isNullOrBlank() -> uri.encodedQuery
|
||||
!uri.encodedFragment.isNullOrBlank() -> uri.encodedFragment
|
||||
else -> null
|
||||
}
|
||||
?.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) {
|
||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||
val matchResult = regex.find(data.fragment.toString())
|
||||
if (matchResult?.groups?.get(1) != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
returnToSettings()
|
||||
}
|
||||
private suspend fun handleAniList(accessToken: String?) {
|
||||
if (accessToken != null) {
|
||||
trackerManager.aniList.login(accessToken)
|
||||
} else {
|
||||
trackerManager.aniList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBangumi(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleBangumi(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.bangumi.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.bangumi.login(code)
|
||||
} else {
|
||||
trackerManager.bangumi.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMyAnimeList(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleMyAnimeList(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.myAnimeList.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.myAnimeList.login(code)
|
||||
} else {
|
||||
trackerManager.myAnimeList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShikimori(data: Uri) {
|
||||
val code = data.getQueryParameter("code")
|
||||
private suspend fun handleShikimori(code: String?) {
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackerManager.shikimori.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
trackerManager.shikimori.login(code)
|
||||
} else {
|
||||
trackerManager.shikimori.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.preference.asState
|
||||
@@ -27,11 +28,16 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
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.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
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.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.model.applyFilter
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
@@ -57,6 +65,7 @@ class UpdatesScreenModel(
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
||||
|
||||
@@ -75,19 +84,35 @@ class UpdatesScreenModel(
|
||||
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
|
||||
|
||||
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,
|
||||
downloadManager.queueState,
|
||||
) { updates, _, _ -> updates }
|
||||
.catch {
|
||||
logcat(LogPriority.ERROR, it)
|
||||
_events.send(Event.InternalError)
|
||||
}
|
||||
.collectLatest { updates ->
|
||||
// needed for Kotlin filters (downloaded)
|
||||
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
|
||||
old.filterDownloaded == new.filterDownloaded
|
||||
},
|
||||
) { updates, _, _, itemPreferences ->
|
||||
updates
|
||||
.toUpdateItems()
|
||||
.applyFilters(itemPreferences)
|
||||
.toPersistentList()
|
||||
}
|
||||
.collectLatest { updateItems ->
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
items = updates.toUpdateItems(),
|
||||
items = updateItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,9 +123,43 @@ class UpdatesScreenModel(
|
||||
.catch { logcat(LogPriority.ERROR, it) }
|
||||
.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
|
||||
.map { update ->
|
||||
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
|
||||
@@ -123,7 +182,6 @@ class UpdatesScreenModel(
|
||||
selected = update.chapterId in selectedChapterIds,
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
fun updateLibrary(): Boolean {
|
||||
@@ -267,7 +325,6 @@ class UpdatesScreenModel(
|
||||
fun toggleSelection(
|
||||
item: UpdatesItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
mutableState.update { state ->
|
||||
@@ -282,7 +339,7 @@ class UpdatesScreenModel(
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (selected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
@@ -308,7 +365,7 @@ class UpdatesScreenModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
} else if (!fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
@@ -361,9 +418,41 @@ class UpdatesScreenModel(
|
||||
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
|
||||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val items: PersistentList<UpdatesItem> = persistentListOf(),
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
@@ -387,6 +476,7 @@ class UpdatesScreenModel(
|
||||
|
||||
sealed interface Dialog {
|
||||
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
|
||||
data object FilterSheet : Dialog
|
||||
}
|
||||
|
||||
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
|
||||
data class UpdatesItem(
|
||||
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 eu.kanade.presentation.updates.UpdateScreen
|
||||
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
|
||||
import eu.kanade.presentation.updates.UpdatesFilterDialog
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||
@@ -54,6 +55,7 @@ data object UpdatesTab : Tab {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { UpdatesScreenModel() }
|
||||
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
UpdateScreen(
|
||||
@@ -74,6 +76,8 @@ data object UpdatesTab : Tab {
|
||||
context.startActivity(intent)
|
||||
},
|
||||
onCalendarClicked = { navigator.push(UpcomingScreen()) },
|
||||
onFilterClicked = screenModel::showFilterDialog,
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
)
|
||||
|
||||
val onDismissDialog = { screenModel.setDialog(null) }
|
||||
@@ -84,6 +88,12 @@ data object UpdatesTab : Tab {
|
||||
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
|
||||
)
|
||||
}
|
||||
is UpdatesScreenModel.Dialog.FilterSheet -> {
|
||||
UpdatesFilterDialog(
|
||||
onDismissRequest = onDismissDialog,
|
||||
screenModel = settingsScreenModel,
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package androidx.preference
|
||||
/**
|
||||
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
|
||||
*/
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
|
||||
return onBindEditTextListener
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.test
|
||||
|
||||
import android.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
@@ -20,8 +19,7 @@ data class DummyTracker(
|
||||
override val supportsPrivateTracking: Boolean = false,
|
||||
override val isLoggedIn: Boolean = false,
|
||||
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
||||
val valLogoColor: Int = Color.rgb(18, 25, 35),
|
||||
val valLogo: Int = R.drawable.ic_tracker_anilist,
|
||||
val valLogo: Int = R.drawable.brand_anilist,
|
||||
val valStatuses: List<Long> = (1L..6L).toList(),
|
||||
val valReadingStatus: Long = 1L,
|
||||
val valRereadingStatus: Long = 1L,
|
||||
@@ -34,8 +32,6 @@ data class DummyTracker(
|
||||
override val client: OkHttpClient
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun getLogoColor(): Int = valLogoColor
|
||||
|
||||
override fun getLogo(): Int = valLogo
|
||||
|
||||
override fun getStatusList(): List<Long> = valStatuses
|
||||
|
||||
@@ -75,6 +75,12 @@ class ShellInterface : IShellInterface.Stub() {
|
||||
.invoke(pmInterface)
|
||||
|
||||
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) {
|
||||
setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
|
||||
}
|
||||
@@ -105,26 +111,27 @@ class ShellInterface : IShellInterface.Stub() {
|
||||
.getMethod("openSession", Int::class.java)
|
||||
.invoke(packageInstaller, sessionId)
|
||||
|
||||
(
|
||||
session::class.java.getMethod(
|
||||
"openWrite",
|
||||
String::class.java,
|
||||
Long::class.java,
|
||||
Long::class.java,
|
||||
).invoke(session, "extension", 0L, apk.length) as ParcelFileDescriptor
|
||||
).let { fd ->
|
||||
val revocable = Class.forName("android.os.SystemProperties")
|
||||
.getMethod("getBoolean", String::class.java, Boolean::class.java)
|
||||
.invoke(null, "fw.revocable_fd", false) as Boolean
|
||||
session::class.java.getMethod(
|
||||
"openWrite",
|
||||
String::class.java,
|
||||
Long::class.java,
|
||||
Long::class.java,
|
||||
)
|
||||
.invoke(session, "extension", 0L, apk.length)
|
||||
.let { it as ParcelFileDescriptor }
|
||||
.let { fd ->
|
||||
val revocable = Class.forName("android.os.SystemProperties")
|
||||
.getMethod("getBoolean", String::class.java, Boolean::class.java)
|
||||
.invoke(null, "fw.revocable_fd", false) as Boolean
|
||||
|
||||
if (revocable) {
|
||||
ParcelFileDescriptor.AutoCloseOutputStream(fd)
|
||||
} else {
|
||||
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
|
||||
.getConstructor(ParcelFileDescriptor::class.java)
|
||||
.newInstance(fd) as OutputStream
|
||||
if (revocable) {
|
||||
ParcelFileDescriptor.AutoCloseOutputStream(fd)
|
||||
} else {
|
||||
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
|
||||
.getConstructor(ParcelFileDescriptor::class.java)
|
||||
.newInstance(fd) as OutputStream
|
||||
}
|
||||
}
|
||||
}
|
||||
.use { output ->
|
||||
apk.createInputStream().use { input -> input.copyTo(output) }
|
||||
}
|
||||
@@ -174,3 +181,7 @@ class ShellInterface : IShellInterface.Stub() {
|
||||
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.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -61,7 +62,6 @@ import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
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.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -143,7 +143,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
|
||||
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
|
||||
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 ->
|
||||
val updatedSources = action(state.sources)
|
||||
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
|
||||
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
if (save) saveSources()
|
||||
saveSources()
|
||||
}
|
||||
|
||||
private fun initSources() {
|
||||
@@ -368,7 +368,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
|
||||
}
|
||||
.toList()
|
||||
|
||||
updateSources(save = false) { sources }
|
||||
mutableState.update { state ->
|
||||
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelection(id: Long) {
|
||||
|
||||
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
|
||||
}
|
||||
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
State(
|
||||
current = current,
|
||||
target = target,
|
||||
applicableFlags = applicableFlags,
|
||||
|
||||
@@ -41,8 +41,8 @@ abstract class BaseSmartSearchEngine<T>(
|
||||
val eligibleManga = supervisorScope {
|
||||
queries.map { query ->
|
||||
async(Dispatchers.Default) {
|
||||
val builtQuery = if (extraSearchParams != null) {
|
||||
"$query ${extraSearchParams.trim()}"
|
||||
val builtQuery = if (!extraSearchParams.isNullOrBlank()) {
|
||||
"$query $extraSearchParams"
|
||||
} else {
|
||||
query
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package mihon.feature.migration.list.search
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import mihon.domain.manga.model.toDomainManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@@ -23,6 +22,6 @@ class SmartSourceSearchEngine(extraSearchParams: String?) : BaseSmartSearchEngin
|
||||
}
|
||||
|
||||
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 {
|
||||
IconButton(onClick = onPreviousClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
|
||||
}
|
||||
IconButton(onClick = onNextClick) {
|
||||
@Suppress("DEPRECATION")
|
||||
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>
|
||||