mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-17 06:02:28 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9670d8a12 | ||
|
|
87e808548b | ||
|
|
51b61a36e8 | ||
|
|
010ac71a84 | ||
|
|
9dbb7148c5 | ||
|
|
a999e49f2f | ||
|
|
ae749bb4f8 | ||
|
|
649fc83ffa | ||
|
|
6dffca1593 | ||
|
|
07b9fb695c | ||
|
|
c279291e29 | ||
|
|
abb126ad20 | ||
|
|
8457fb91e0 | ||
|
|
143a060ac9 | ||
|
|
8912ef5b5c | ||
|
|
79fc3a0a8a | ||
|
|
c8b820a08f | ||
|
|
f45d0ad31d | ||
|
|
b5856135c5 | ||
|
|
039138ce99 | ||
|
|
35bd0a4b35 | ||
|
|
dd4e9f00f1 | ||
|
|
bce1f71958 | ||
|
|
d58130a5fc | ||
|
|
e562a9b4f8 | ||
|
|
ba44815254 | ||
|
|
fc20410ceb | ||
|
|
b454c1b469 | ||
|
|
f02bf5ba7e | ||
|
|
ba16158b13 | ||
|
|
8066b8dfc9 | ||
|
|
260e31cd2c | ||
|
|
b15af71b94 | ||
|
|
3e5d593e28 | ||
|
|
c0c0909999 | ||
|
|
c46317f099 | ||
|
|
f9c5fa9011 | ||
|
|
a82d27c3a6 | ||
|
|
611578313e | ||
|
|
f921ec07a6 | ||
|
|
e21414b229 | ||
|
|
e42e01049d | ||
|
|
6c1b0702ba | ||
|
|
292381662a | ||
|
|
44c1e1915d | ||
|
|
8972882a34 | ||
|
|
e9234f205d | ||
|
|
adc9ee74ec | ||
|
|
be44a81fb6 | ||
|
|
88d4071cce | ||
|
|
f28b2d887e | ||
|
|
287cbbb986 | ||
|
|
d3f10772f2 | ||
|
|
785f4fd1ed | ||
|
|
40f552981a | ||
|
|
8c0419657b | ||
|
|
36ec043a5c | ||
|
|
0c1be369b0 | ||
|
|
157679bba5 | ||
|
|
5b3505bf5f | ||
|
|
2f5b9c1641 | ||
|
|
24bd3c95a5 | ||
|
|
7f2b4962b7 | ||
|
|
8d39153569 | ||
|
|
e998904fb6 | ||
|
|
d1e71ab58f | ||
|
|
a80027ff36 | ||
|
|
dd46413e10 | ||
|
|
ab7ee00c48 | ||
|
|
fd4f73c908 | ||
|
|
a6ee4bc596 | ||
|
|
6d4ed6cb22 | ||
|
|
76b146f3d4 | ||
|
|
5910a51eb4 | ||
|
|
1e471a414c | ||
|
|
06d6128116 | ||
|
|
6a329386c2 | ||
|
|
179bfaca72 | ||
|
|
5df78e305e | ||
|
|
940272f75a | ||
|
|
7ff97761b9 | ||
|
|
9a6dea804d | ||
|
|
fb8e640d69 | ||
|
|
1f85a5a8db | ||
|
|
730e43fd14 | ||
|
|
61cd786e7f | ||
|
|
6490c41e63 | ||
|
|
a5d584eeb0 | ||
|
|
f027de8c2e | ||
|
|
7aacaa3490 | ||
|
|
f82a7a68f4 | ||
|
|
d1c5c12bdd | ||
|
|
f35bc341ae | ||
|
|
a7a8cc8343 | ||
|
|
4c88f02646 | ||
|
|
a4780e4edb | ||
|
|
db45dda525 | ||
|
|
9abc830769 | ||
|
|
9409740d7e | ||
|
|
38e93086c8 | ||
|
|
01d302febd | ||
|
|
6dfa10ffac | ||
|
|
f2464a1ba6 | ||
|
|
98ba693054 | ||
|
|
972c9d43da | ||
|
|
866d045c7a | ||
|
|
65d7110e1e | ||
|
|
2d1d2e4cab | ||
|
|
93cc075116 | ||
|
|
ec614bb4c3 | ||
|
|
59fdf7e7fa | ||
|
|
7451c228e6 | ||
|
|
9b5e0ae735 | ||
|
|
7f4c55176f | ||
|
|
9b4c3115db | ||
|
|
f835398094 | ||
|
|
3542143b40 | ||
|
|
ba4c04fe74 | ||
|
|
087e38ad47 | ||
|
|
09edf7dc8d | ||
|
|
4f33486aa4 | ||
|
|
2d86a70ce2 | ||
|
|
0938411053 | ||
|
|
ed17ece4e3 | ||
|
|
b74e010bfe | ||
|
|
4b48a84ec9 | ||
|
|
9a77baedcc | ||
|
|
2b4b8472bb | ||
|
|
553762fae8 | ||
|
|
8bfa495f34 | ||
|
|
fc5592ff75 | ||
|
|
0044cc73ec | ||
|
|
6ee529c5aa | ||
|
|
4df0e6b168 | ||
|
|
986f46c09a | ||
|
|
f8fff318ba | ||
|
|
306befb214 | ||
|
|
a4abb6292b | ||
|
|
c67a33f3d4 | ||
|
|
532575e29e | ||
|
|
f52d890e7f | ||
|
|
449c4e08e7 | ||
|
|
57666e9207 | ||
|
|
73fef961e9 | ||
|
|
93a163f96e | ||
|
|
61680580dd | ||
|
|
0de91a9c53 | ||
|
|
2c979d2d32 | ||
|
|
2b9cb4ca5a | ||
|
|
427e105840 | ||
|
|
12fbb13df5 | ||
|
|
c69b14c975 | ||
|
|
debab6c261 | ||
|
|
424bbc53b8 | ||
|
|
71cd94a31d | ||
|
|
01e3f8c605 | ||
|
|
504ec2afae | ||
|
|
385d076d43 | ||
|
|
5848ffb4c4 | ||
|
+13 |
51efde975b | ||
|
|
1e142c4255 | ||
|
|
a7179805ad | ||
|
|
1c43addeca | ||
|
|
b2cda03e3a | ||
|
|
fdc2d40a1d | ||
|
|
e6d8efe6f8 | ||
|
|
0e0bc6972a | ||
|
|
a5cd541bdf | ||
|
|
0fbaa1ecec | ||
|
+21 |
cbafe9bf3f | ||
|
|
7418f4e7bc | ||
|
|
2d1a8bbeff | ||
|
|
60e74b8d8f | ||
|
|
6ef6e81814 | ||
|
|
855918bbf4 | ||
|
|
6c6eff3ab8 | ||
|
|
d435dd9217 | ||
|
|
f7da5c856a | ||
|
|
737fdbfbf0 | ||
|
|
802958b1ea | ||
|
|
2292554d97 | ||
|
|
918cfb3dda | ||
|
|
5de262675a | ||
|
|
9f480564c2 | ||
|
|
d17ad1e002 | ||
|
|
1e054ea14d | ||
|
|
5f87f7e054 | ||
|
|
a5260487d3 | ||
|
|
0ce6f22cf5 | ||
|
|
35bb84705e | ||
|
|
e32231eeaa | ||
|
|
d0ee514044 | ||
|
|
44a9d96e90 | ||
|
|
3f845c12c8 | ||
|
|
6c0fa3a043 | ||
|
|
2810dd14c4 | ||
|
|
aebf11a749 | ||
|
|
3a64c8d65c | ||
|
|
f7cd820f2f | ||
|
|
5b69dbad81 | ||
|
|
a82baac3ef | ||
|
|
5e99306e5f | ||
|
|
3acd7eaca9 | ||
|
|
f3125ab897 | ||
|
|
21af65b100 | ||
|
|
056d9baeeb | ||
|
|
baad5c59bb | ||
|
|
34a33ed4f8 | ||
|
|
7f342ca076 | ||
|
|
b695940114 | ||
|
|
bb24e904a9 | ||
|
|
5fadea61e7 | ||
|
|
c1d1f6b161 | ||
|
|
ff3dedb6d8 | ||
|
|
7b41ebf8fe | ||
|
|
161617b95d | ||
|
|
1512ca63d7 | ||
|
|
6cfce8d425 | ||
|
|
2776e3edda | ||
|
|
7847cad0f4 | ||
|
|
deb7b33118 | ||
|
|
3f56685e0c | ||
|
|
0854bde12e | ||
|
|
ac249e3668 | ||
|
|
e3813028bd | ||
|
|
270b78ba95 | ||
|
|
46c8f07c12 | ||
|
|
a2c24ff042 | ||
|
|
9ec479266b | ||
|
|
6458246180 | ||
|
|
31d3c23717 | ||
|
|
497e266256 | ||
|
|
0974e787ad | ||
|
|
01bfaa99ad | ||
|
|
4b5a0539e5 | ||
|
|
598f634afb | ||
|
|
dee3daf145 | ||
|
|
9fe2c2e106 | ||
|
|
3cc0d2c324 | ||
|
|
f75f2598a3 | ||
|
|
c613b52503 | ||
|
|
8586d81e63 | ||
|
|
3e98c15b74 | ||
|
+1 |
2b532ea0c4 | ||
|
|
b2015d1ef3 | ||
|
|
1e5b1dc5e7 | ||
|
|
5fa5f7e159 | ||
|
|
00fb597ae9 | ||
|
|
7e78849ed0 | ||
|
|
4d5b0ea906 | ||
|
|
a81b7bce9a | ||
|
|
abdecb3cb4 | ||
|
|
ed30766a9f | ||
|
|
b1efee47f5 | ||
|
|
d00efa4bfc | ||
|
|
b1a9a05926 | ||
|
|
55675e7f80 | ||
|
|
b393088cfa | ||
|
|
77e88a2159 | ||
|
|
ecbec8f501 | ||
|
|
5c3ae37d52 | ||
|
|
581a89b7b0 | ||
|
|
6197768a01 | ||
|
|
d1f9a1fdaf | ||
|
|
db7f1663b7 | ||
|
|
a0a3adf864 | ||
|
|
437dd87225 | ||
|
|
cd1f426945 | ||
|
|
2506b04964 | ||
|
|
77a1a5afbd | ||
|
|
a494ebd791 | ||
|
|
c68efd0e86 | ||
|
|
ae37c1651a | ||
|
|
62288c08fb | ||
|
|
3cb9883d62 | ||
|
|
fab4cbd279 | ||
|
|
6c0058d230 | ||
|
|
2aec7d19c0 | ||
|
|
bb9767c9df | ||
|
|
864281af7d | ||
|
|
f6aad7497e | ||
|
|
0834884335 | ||
|
|
ed274e26fd | ||
|
|
f9156aa17a | ||
|
|
b6aa40a849 | ||
|
|
68aa4b003f | ||
|
|
aa25b3a14c | ||
|
|
869fa1a4b2 | ||
|
|
df6507256a | ||
|
+3 |
b765531b82 | ||
|
|
21c6357967 | ||
|
|
3d20bcc6b6 | ||
|
|
71deefb6fd | ||
|
|
8fceb737a4 | ||
|
|
81e0754c0d | ||
|
|
e287f08789 | ||
|
|
a73271aefa | ||
|
|
f7a1ecd251 | ||
|
|
f80c91081f | ||
|
|
55be95dd5d | ||
|
|
d7f3ceef5c | ||
|
|
1d8a2b05dd | ||
|
|
6d69903a56 | ||
|
|
0648e2eaaf | ||
|
|
7d4e1476ba | ||
|
+9 |
46a2471abb | ||
|
|
f63dc799f0 | ||
|
|
c114028a33 | ||
|
|
4296d056b5 | ||
|
|
7aaa891aef | ||
|
|
2ccb0ca163 | ||
|
|
44cd9a55d1 | ||
|
|
2a92f0b46a | ||
|
|
f37e81991e | ||
|
|
d15a05602a | ||
|
|
fbf5ae82f7 | ||
|
|
fdb223d657 | ||
|
|
54ae69eaa2 | ||
|
|
45b1e781eb | ||
|
|
069e5c91ac | ||
|
|
4ac9f3474b | ||
|
|
9c3cc1ca5f | ||
|
|
47d7c0f2ea | ||
|
|
0b5e9bb815 | ||
|
|
d72ae365cb | ||
|
|
e7ec672c8b | ||
|
|
628cb58862 | ||
|
|
e454cf8e9d | ||
|
|
baa0fb7ecb | ||
|
|
2d2cf0879f | ||
|
|
6981185f4b | ||
|
|
47aa9190d4 | ||
|
|
98705910e7 | ||
|
|
5ce7d00eb8 | ||
|
|
03229a3807 | ||
|
|
493140c7ff | ||
|
|
98bb731b4d | ||
|
|
c3b99aea07 | ||
|
|
ef1c529672 | ||
|
|
633eea4ced | ||
|
|
31a4d6ba39 | ||
|
|
e9b7c760a1 | ||
|
|
ef1e7ba907 | ||
|
|
526d79a975 | ||
|
|
b9ac9a0422 | ||
|
|
d171768cff | ||
|
|
cb2617dee0 | ||
|
|
b4635c41a8 | ||
|
|
217b20123c | ||
|
|
b2ca6bc5f3 |
@@ -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.20.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.21.1](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.20.1"
|
||||
Example: "0.21.1"
|
||||
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.20.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.21.1](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
|
||||
|
||||
@@ -23,24 +23,26 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Test App
|
||||
runs-on: 'ubuntu-24.04'
|
||||
runs-on: 'ubuntu-26.04'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
@@ -54,22 +56,22 @@ jobs:
|
||||
|
||||
- name: Upload test report
|
||||
if: steps.unit_tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: test-report-${{ github.sha }}
|
||||
path: app/build/reports/tests/testDebugUnitTest
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
run: ./gradlew assembleRelease -Pdist=ci
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
get_tag:
|
||||
if: github.repository == 'mihonapp/mihon'
|
||||
name: Extract tag name
|
||||
runs-on: 'ubuntu-24.04'
|
||||
runs-on: 'ubuntu-26.04'
|
||||
outputs:
|
||||
tag: ${{ steps.extract.outputs.tag }}
|
||||
|
||||
@@ -27,26 +27,33 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'mihonapp/mihon'
|
||||
name: Build
|
||||
runs-on: 'ubuntu-24.04'
|
||||
runs-on: 'ubuntu-26.04'
|
||||
needs: get_tag
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Decode signing keystore
|
||||
run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore"
|
||||
env:
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
run: ./gradlew assembleRelease -Pdist=github
|
||||
env:
|
||||
storeFileBase64: ${{ secrets.SIGNING_KEY }}
|
||||
storeFile: ${{ runner.temp }}/antsy.keystore
|
||||
storePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyAlias: ${{ secrets.ALIAS }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
@@ -62,7 +69,7 @@ jobs:
|
||||
mv app/build/outputs/apk/release/app-x86_64-release.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: mihon
|
||||
path: |
|
||||
@@ -75,28 +82,33 @@ jobs:
|
||||
build_foss:
|
||||
if: github.repository == 'mihonapp/mihon'
|
||||
name: Build (FOSS)
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
needs: get_tag
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleFoss -Penable-updater
|
||||
- name: Decode signing keystore
|
||||
run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore"
|
||||
env:
|
||||
storeFileBase64: ${{ secrets.SIGNING_KEY }}
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleFoss -Pdist=foss
|
||||
env:
|
||||
storeFile: ${{ runner.temp }}/antsy.keystore
|
||||
storePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyAlias: ${{ secrets.ALIAS }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
@@ -108,7 +120,7 @@ jobs:
|
||||
mv app/build/outputs/apk/foss/app-universal-foss.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: mihon-foss
|
||||
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||
@@ -116,12 +128,12 @@ jobs:
|
||||
release:
|
||||
if: github.repository == 'mihonapp/mihon'
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-26.04
|
||||
needs: [get_tag, build, build_foss]
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
@@ -134,7 +146,7 @@ jobs:
|
||||
mihon-foss
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update_website:
|
||||
runs-on: 'ubuntu-24.04'
|
||||
runs-on: 'ubuntu-26.04'
|
||||
|
||||
steps:
|
||||
- name: Update website
|
||||
|
||||
+200
-2
@@ -11,9 +11,202 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- `Other` - for technical stuff.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.21.1] - 2026-10-12
|
||||
### Fixed
|
||||
- Fixed refreshing chapters failing with the app language set to Arabic or another language with its own digits ([@AntsyLich](https://github.com/AntsyLich)) ([`8457fb9`](https://github.com/mihonapp/mihon/commit/8457fb9))
|
||||
- Fixed a crash on launch on some Samsung devices running Android 9 to 11 ([@AntsyLich](https://github.com/AntsyLich)) ([`abb126a`](https://github.com/mihonapp/mihon/commit/abb126a))
|
||||
- Fixed the start and resume button going under the taskbar on tablets ([@AntsyLich](https://github.com/AntsyLich)) ([`c279291`](https://github.com/mihonapp/mihon/commit/c279291))
|
||||
- Fixed a crash when updating an extension while others were installing ([@AntsyLich](https://github.com/AntsyLich)) ([`07b9fb6`](https://github.com/mihonapp/mihon/commit/07b9fb6))
|
||||
- Fixed library entries without a date added getting their last update time as date added when updating from v0.20.x ([@AntsyLich](https://github.com/AntsyLich)) ([`6dffca1`](https://github.com/mihonapp/mihon/commit/6dffca1))
|
||||
- Fixed the app closing when WebView's page process crashed ([@AntsyLich](https://github.com/AntsyLich)) ([`649fc83`](https://github.com/mihonapp/mihon/commit/649fc83))
|
||||
- Fixed installing or updating an extension sometimes cancelling the install of another one ([@AntsyLich](https://github.com/AntsyLich)) ([`ae749bb`](https://github.com/mihonapp/mihon/commit/ae749bb))
|
||||
- Fixed updating from v0.20.x taking up to hours on libraries with many duplicate entries ([@AntsyLich](https://github.com/AntsyLich)) ([`a999e49`](https://github.com/mihonapp/mihon/commit/a999e49))
|
||||
- Fixed the app staying on the updating screen for good when updating its data failed, it now shows what went wrong, and updating failing on leftover data of deleted entries ([@AntsyLich](https://github.com/AntsyLich)) ([`9dbb714`](https://github.com/mihonapp/mihon/commit/9dbb714))
|
||||
- Fixed library updates ignoring the smart update restrictions for entries whose details were never fetched ([@AntsyLich](https://github.com/AntsyLich)) ([`010ac71`](https://github.com/mihonapp/mihon/commit/010ac71))
|
||||
- Fixed resuming a chapter sometimes opening the next chapter partway through, and saving progress to it, in the right to left reader ([@AntsyLich](https://github.com/AntsyLich)) ([`51b61a3`](https://github.com/mihonapp/mihon/commit/51b61a3))
|
||||
|
||||
## [v0.21.0] - 2026-10-12
|
||||
### Added
|
||||
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya), [@BrutuZ](https://github.com/BrutuZ)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
|
||||
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
|
||||
- Fix a crash after logging in to Kitsu caused by the rating system's letter case ([@Secozzi](https://github.com/Secozzi)) ([#3887](https://github.com/mihonapp/mihon/pull/3887))
|
||||
- Add option to keep loading installed extensions regardless of the allowed content warnings ([@AntsyLich](https://github.com/AntsyLich)) ([#3952](https://github.com/mihonapp/mihon/pull/3952))
|
||||
- Read chapter dates from the Year, Month, and Day fields of ComicInfo.xml in local source, and write them when downloading chapters ([@MajorTanya](https://github.com/MajorTanya)) ([#3967](https://github.com/mihonapp/mihon/pull/3967))
|
||||
- Fix wrongly padded years and months above 12 in local source dates ([@MajorTanya](https://github.com/MajorTanya)) ([#3970](https://github.com/mihonapp/mihon/pull/3970))
|
||||
- Add refresh buttons to trackers in Settings to update displayed usernames & rating systems (where supported) ([@MajorTanya](https://github.com/MajorTanya)) ([#3828](https://github.com/mihonapp/mihon/pull/3828))
|
||||
- Add option to turn off the hash suffix in downloaded chapter folder names (Settings → Advanced). It stays on for existing installs ([@choppeh](https://github.com/choppeh), [@AntsyLich](https://github.com/AntsyLich)) ([#3966](https://github.com/mihonapp/mihon/pull/3966))
|
||||
- Fixed downloads not being found when the scanlator name contains a "/" ([@AntsyLich](https://github.com/AntsyLich)) ([`a5d584e`](https://github.com/mihonapp/mihon/commit/a5d584e))
|
||||
- Show a screen while the app migrates its data after an update ([@AntsyLich](https://github.com/AntsyLich)) ([`fd4f73c`](https://github.com/mihonapp/mihon/commit/fd4f73c))
|
||||
- Keep the screen up for at least a second so it doesn't just flash by ([@AntsyLich](https://github.com/AntsyLich)) ([`b454c1b`](https://github.com/mihonapp/mihon/commit/b454c1b))
|
||||
- Add option to skip an app update version ([@AntsyLich](https://github.com/AntsyLich)) ([`fc20410`](https://github.com/mihonapp/mihon/commit/fc20410))
|
||||
- Add support for [Comick](https://comick.dev) tracker ([@MajorTanya](https://github.com/MajorTanya)) ([#4056](https://github.com/mihonapp/mihon/pull/4056))
|
||||
|
||||
### Changed
|
||||
- Download app updates from the update screen instead of a background job, and remove the "App updates" notification channel ([@AntsyLich](https://github.com/AntsyLich)) ([#3707](https://github.com/mihonapp/mihon/pull/3707))
|
||||
- Switch app icons to Material Symbols ([@AntsyLich](https://github.com/AntsyLich)) ([#3873](https://github.com/mihonapp/mihon/pull/3873))
|
||||
- Use the filled icon for the library continue reading button ([@AntsyLich](https://github.com/AntsyLich)) ([`34a33ed`](https://github.com/mihonapp/mihon/commit/34a33ed))
|
||||
- Move notification, launcher shortcut, bottom navigation and reading mode icons to Material Symbols, bring back filled icons where the switch dropped them, and show a compact grid icon for the compact display modes ([@AntsyLich](https://github.com/AntsyLich)) ([#4057](https://github.com/mihonapp/mihon/pull/4057))
|
||||
- Replace the NSFW sources toggle with allowed content warnings (Safe, Mixed, 18+) for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3951](https://github.com/mihonapp/mihon/pull/3951))
|
||||
- Restoring a backup over an existing library now keeps existing entry details, the furthest read progress and the earliest date added ([@AntsyLich](https://github.com/AntsyLich)) ([`c67a33f`](https://github.com/mihonapp/mihon/commit/c67a33f))
|
||||
- Install and update extensions only from a repo whose signing key matches the extension, and reject downloaded extensions not signed with their repo's key ([@AntsyLich](https://github.com/AntsyLich)) ([`0938411`](https://github.com/mihonapp/mihon/commit/0938411))
|
||||
- Keep the repo shown for an installed extension current when repos are renamed or removed ([@AntsyLich](https://github.com/AntsyLich)) ([`09edf7d`](https://github.com/mihonapp/mihon/commit/09edf7d))
|
||||
- Show the repo name in extension details, opening the repo's website, instead of the "Open source repo" button ([@AntsyLich](https://github.com/AntsyLich)) ([#3955](https://github.com/mihonapp/mihon/pull/3955))
|
||||
- Rework the database schema, merging duplicate entries and chapters left by older versions. The merge can't be undone, so create a backup before updating ([@AntsyLich](https://github.com/AntsyLich)) ([#3805](https://github.com/mihonapp/mihon/pull/3805))
|
||||
- Fixed restoring a backup failing on entries that list a category or excluded scanlator twice ([@AntsyLich](https://github.com/AntsyLich)) ([#4022](https://github.com/mihonapp/mihon/pull/4022))
|
||||
- List MangaBaka first among trackers ([@AntsyLich](https://github.com/AntsyLich)) ([`dd46413`](https://github.com/mihonapp/mihon/commit/dd46413))
|
||||
- Show a wavy progress ring while a chapter downloads ([@AntsyLich](https://github.com/AntsyLich)) ([`e562a9b`](https://github.com/mihonapp/mihon/commit/e562a9b))
|
||||
- Show a confirmation dialog when unfavoriting manga from the manga screen ([@WizaHX](https://github.com/WizaHX)) ([#4073](https://github.com/mihonapp/mihon/pull/4073))
|
||||
|
||||
### Improved
|
||||
- Show the updates and upcoming filter icon as active when filtering by category, or by hidden excluded scanlators in updates ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||
- Load extensions in the background to speed up app startup ([@AntsyLich](https://github.com/AntsyLich)) ([#3788](https://github.com/mihonapp/mihon/pull/3788))
|
||||
- Wait for extensions to load before listing sources, so lists and global search aren't empty right after launch ([@AntsyLich](https://github.com/AntsyLich)) ([#3841](https://github.com/mihonapp/mihon/pull/3841))
|
||||
- Show scores in MangaUpdates search results (and authors for `id:` prefix searches) ([@MajorTanya](https://github.com/MajorTanya)) ([#3795](https://github.com/mihonapp/mihon/pull/3795))
|
||||
- Remove whitespace from MAL and MB `id:` prefix search inputs before searching ([@MajorTanya](https://github.com/MajorTanya)) ([#3793](https://github.com/mihonapp/mihon/pull/3793))
|
||||
- Detect Cloudflare challenges with Cloudflare's `cf-mitigated` header, and fail sooner when a challenge needs user interaction instead of waiting for the timeout ([@tigeryu8900](https://github.com/tigeryu8900)) ([#3842](https://github.com/mihonapp/mihon/pull/3842))
|
||||
- Show a helpful error message for expired AniList credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#3888](https://github.com/mihonapp/mihon/pull/3888))
|
||||
- Show installed extensions that couldn't be loaded, with the reason and a copyable stack trace ([@AntsyLich](https://github.com/AntsyLich)) ([#3953](https://github.com/mihonapp/mihon/pull/3953))
|
||||
- Apply extension trust, repo and content warning changes without restarting the app ([@AntsyLich](https://github.com/AntsyLich)) ([#3954](https://github.com/mihonapp/mihon/pull/3954))
|
||||
- Use the system document picker when selecting a backup to restore ([@MajorTanya](https://github.com/MajorTanya)) ([#3948](https://github.com/mihonapp/mihon/pull/3948))
|
||||
- Offer updates for installed extensions that aren't loaded ([@AntsyLich](https://github.com/AntsyLich)) ([`2d86a70`](https://github.com/mihonapp/mihon/commit/2d86a70))
|
||||
- Show available extension updates on the Browse badge and in the Extensions tab right after launch ([@AntsyLich](https://github.com/AntsyLich)) ([`4f33486`](https://github.com/mihonapp/mihon/commit/4f33486))
|
||||
- Show which repo an extension comes from in the extensions list, and list an extension once for each repo signing key offering it ([@AntsyLich](https://github.com/AntsyLich)) ([#3955](https://github.com/mihonapp/mihon/pull/3955))
|
||||
- Speed up loading the library on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#3805](https://github.com/mihonapp/mihon/pull/3805))
|
||||
- Speed up history, updates, the updates widget and browse on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#4022](https://github.com/mihonapp/mihon/pull/4022))
|
||||
- Reduce lag when dragging sliders in settings ([@AntsyLich](https://github.com/AntsyLich)) ([`9409740`](https://github.com/mihonapp/mihon/commit/9409740))
|
||||
- Speed up refreshing the library when returning to it ([@AntsyLich](https://github.com/AntsyLich)) ([`f027de8`](https://github.com/mihonapp/mihon/commit/f027de8))
|
||||
- Download a source's chapters faster by starting the next one while the current one finishes ([@AntsyLich](https://github.com/AntsyLich)) ([`730e43f`](https://github.com/mihonapp/mihon/commit/730e43f))
|
||||
- Load an entry's cover once when several updates show it ([@AntsyLich](https://github.com/AntsyLich)) ([`1f85a5a`](https://github.com/mihonapp/mihon/commit/1f85a5a))
|
||||
- Speed up the entry details chapter list on entries with many chapters, and the updates list when many chapters are queued for download ([@AntsyLich](https://github.com/AntsyLich)) ([`fb8e640`](https://github.com/mihonapp/mihon/commit/fb8e640))
|
||||
- Speed up checking which chapters are downloaded ([@AntsyLich](https://github.com/AntsyLich)) ([`6a32938`](https://github.com/mihonapp/mihon/commit/6a32938))
|
||||
- Stop the entry details screen from refreshing its chapter list in the background while reading ([@AntsyLich](https://github.com/AntsyLich)) ([`7ff9776`](https://github.com/mihonapp/mihon/commit/7ff9776))
|
||||
- Speed up opening the library on large libraries by loading tracker entries only for tracker filters and sorting ([@AntsyLich](https://github.com/AntsyLich)) ([`940272f`](https://github.com/mihonapp/mihon/commit/940272f))
|
||||
- Speed up the library query on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([`5df78e3`](https://github.com/mihonapp/mihon/commit/5df78e3))
|
||||
- Speed up the statistics screen on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([`179bfac`](https://github.com/mihonapp/mihon/commit/179bfac))
|
||||
- Find settings by their group heading in settings search ([@kaosjammo](https://github.com/kaosjammo), [@AntsyLich](https://github.com/AntsyLich)) ([#4044](https://github.com/mihonapp/mihon/pull/4044))
|
||||
- Predict the next library update from release patterns such as fixed weekdays or a day of the month, ignore chapters dated in the future, and include the predicted date in backups ([@AntsyLich](https://github.com/AntsyLich)) ([`2923816`](https://github.com/mihonapp/mihon/commit/2923816))
|
||||
- Also follow two days of the month, a weekday of the month, releases in batches and breaks between seasons, and never wait more than 28 days between checks ([@AntsyLich](https://github.com/AntsyLich)) ([`ba44815`](https://github.com/mihonapp/mihon/commit/ba44815))
|
||||
- Recognize chapter numbers more accurately by looking at neighbouring chapters, and stop taking volume numbers or numbers every chapter name shares as the chapter number ([@AntsyLich](https://github.com/AntsyLich)) ([`e42e010`](https://github.com/mihonapp/mihon/commit/e42e010))
|
||||
- Move blocking work off the main thread across the app for fewer freezes and stutters ([@AntsyLich](https://github.com/AntsyLich)) ([#4068](https://github.com/mihonapp/mihon/pull/4068))
|
||||
- Load reader pages in parallel and preload the rest of the chapter and the next chapter, with a "Load ahead only on Wi-Fi" setting to save mobile data ([@AntsyLich](https://github.com/AntsyLich)) ([#2986](https://github.com/mihonapp/mihon/pull/2986))
|
||||
- Cache extension and source icons so lists show them faster ([@AntsyLich](https://github.com/AntsyLich)) ([`b15af71`](https://github.com/mihonapp/mihon/commit/b15af71))
|
||||
- Keep screens loaded while they're hidden so switching back to them is faster ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||
- Speed up the migration list, the extensions list and settings search ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||
- Cancel the previous tracker search when searching again ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||
- Do less background work while reading, downloading and updating the library, and when no updates widget is placed ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||
- Speed up loading extension repos at startup ([@AntsyLich](https://github.com/AntsyLich)) ([`8066b8d`](https://github.com/mihonapp/mihon/commit/8066b8d))
|
||||
- Speed up opening the entry details screen and showing download progress there and in updates ([@AntsyLich](https://github.com/AntsyLich)) ([`ba16158`](https://github.com/mihonapp/mihon/commit/ba16158))
|
||||
- Speed up queueing many chapters for download at once ([@AntsyLich](https://github.com/AntsyLich)) ([`f02bf5b`](https://github.com/mihonapp/mihon/commit/f02bf5b))
|
||||
|
||||
### Removed
|
||||
- Remove "Use legacy decoder for long strip reader" advanced setting, since long strip pages no longer load as one big image ([@AntsyLich](https://github.com/AntsyLich))
|
||||
|
||||
### Fixed
|
||||
- Fixed app and extension update check running again on configuration change ([@AntsyLich](https://github.com/AntsyLich)) ([#3708](https://github.com/mihonapp/mihon/pull/3708))
|
||||
- Fixed MangaBaka user start/finish dates drifting in negative offset timezones ([@MajorTanya](https://github.com/MajorTanya)) ([#3711](https://github.com/mihonapp/mihon/pull/3711))
|
||||
- Fixed MangaBaka scores being wrong when score step size was set to > 1, and being counted 10 times too high in library sorting by tracker score and in statistics ([@MajorTanya](https://github.com/MajorTanya)) ([#3740](https://github.com/mihonapp/mihon/pull/3740))
|
||||
- Fixed tracking an entry on Shikimori failing with "Unknown status: 0" when it isn't in your Shikimori list ([@MajorTanya](https://github.com/MajorTanya)) ([#3810](https://github.com/mihonapp/mihon/pull/3810))
|
||||
- Fixed default category and manga sometimes not getting their category set when restoring a backup ([@Secozzi](https://github.com/Secozzi)) ([#3891](https://github.com/mihonapp/mihon/pull/3891))
|
||||
- Fixed text between dollar signs disappearing from descriptions and notes ([@renovate](https://github.com/apps/renovate)) ([#3806](https://github.com/mihonapp/mihon/pull/3806))
|
||||
- Fixed AniList requests failing with rate limit errors by lowering the request rate to AniList's current limit ([@MajorTanya](https://github.com/MajorTanya)) ([#3942](https://github.com/mihonapp/mihon/pull/3942))
|
||||
- Fixed library search when clicking source name in manga info screen ([@choppeh](https://github.com/choppeh)) ([#4002](https://github.com/mihonapp/mihon/pull/4002))
|
||||
- Fixed entries being incorrectly marked as duplicates when tracked with some trackers ([@Secozzi](https://github.com/Secozzi)) ([#4008](https://github.com/mihonapp/mihon/pull/4008))
|
||||
- Fixed the default chapter settings not being applied to entries added to the library from a source by long pressing them ([@AntsyLich](https://github.com/AntsyLich)) ([`f8fff31`](https://github.com/mihonapp/mihon/commit/f8fff31))
|
||||
- Fixed chapter list updates being partially applied when a refresh fails, and sources that list a chapter twice adding it twice ([@AntsyLich](https://github.com/AntsyLich)) ([`6ee529c`](https://github.com/mihonapp/mihon/commit/6ee529c))
|
||||
- Fixed downloads of entries from uninstalled extensions sometimes not being recognized ([@AntsyLich](https://github.com/AntsyLich)) ([`0044cc7`](https://github.com/mihonapp/mihon/commit/0044cc7))
|
||||
- Fixed a removed extension repo coming back when it was removed while repos were refreshing ([@AntsyLich](https://github.com/AntsyLich)) ([`fc5592f`](https://github.com/mihonapp/mihon/commit/fc5592f))
|
||||
- Fixed being able to rename a category to an empty name ([@AntsyLich](https://github.com/AntsyLich)) ([`8bfa495`](https://github.com/mihonapp/mihon/commit/8bfa495))
|
||||
- Fixed reading time sometimes being counted twice, and tracker start dates being set to 1970 after removing history ([@AntsyLich](https://github.com/AntsyLich)) ([`553762f`](https://github.com/mihonapp/mihon/commit/553762f))
|
||||
- Fixed chapters from excluded scanlators failing to open from notifications or links ([@AntsyLich](https://github.com/AntsyLich)) ([`2b4b847`](https://github.com/mihonapp/mihon/commit/2b4b847))
|
||||
- Fixed queued offline tracker updates being lost when the track was refreshed ([@AntsyLich](https://github.com/AntsyLich)) ([`9a77bae`](https://github.com/mihonapp/mihon/commit/9a77bae))
|
||||
- Fixed restoring a backup mixing up tracker entries of an entry that was already tracked ([@AntsyLich](https://github.com/AntsyLich)) ([`4b48a84`](https://github.com/mihonapp/mihon/commit/4b48a84))
|
||||
- Fixed crashes from APIs extensions use being removed from release builds ([@AntsyLich](https://github.com/AntsyLich)) ([`6dfa10f`](https://github.com/mihonapp/mihon/commit/6dfa10f))
|
||||
- Fixed the download service staying alive after downloads stopped for a network change ([@AntsyLich](https://github.com/AntsyLich)) ([`01d302f`](https://github.com/mihonapp/mihon/commit/01d302f))
|
||||
- Fixed "database is locked" crashes ([@AntsyLich](https://github.com/AntsyLich)) ([`38e9308`](https://github.com/mihonapp/mihon/commit/38e9308))
|
||||
- Use WAL journaling on every device ([@AntsyLich](https://github.com/AntsyLich)) ([`61cd786`](https://github.com/mihonapp/mihon/commit/61cd786))
|
||||
- Fixed global search leaking threads each time it was opened ([@choppeh](https://github.com/choppeh)) ([#4036](https://github.com/mihonapp/mihon/pull/4036))
|
||||
- Fixed sources getting stuck loading in global and migration search ([@choppeh](https://github.com/choppeh)) ([#4037](https://github.com/mihonapp/mihon/pull/4037))
|
||||
- Fixed webtoon reader leaving a gap after zooming in split screen or a resized window ([@AntsyLich](https://github.com/AntsyLich)) ([`d1c5c12`](https://github.com/mihonapp/mihon/commit/d1c5c12))
|
||||
- Fixed download error notifications showing an old time and the previous warning's text ([@AntsyLich](https://github.com/AntsyLich)) ([`f82a7a6`](https://github.com/mihonapp/mihon/commit/f82a7a6))
|
||||
- Fixed the paused downloads notification not appearing ([@AntsyLich](https://github.com/AntsyLich)) ([`7aacaa3`](https://github.com/mihonapp/mihon/commit/7aacaa3))
|
||||
- Fixed duplicate page files and failed downloads after pausing and resuming downloads ([@AntsyLich](https://github.com/AntsyLich)) ([`6490c41`](https://github.com/mihonapp/mihon/commit/6490c41))
|
||||
- Fixed high CPU use on the entry details and updates screens while many chapters are queued for download ([@AntsyLich](https://github.com/AntsyLich)) ([`9a6dea8`](https://github.com/mihonapp/mihon/commit/9a6dea8))
|
||||
- Fixed backups dropping the names of sources whose extension isn't installed, which left them blank under missing sources when restoring ([@AntsyLich](https://github.com/AntsyLich)) ([`5910a51`](https://github.com/mihonapp/mihon/commit/5910a51))
|
||||
- Fixed the updates widget showing "Can't show content" when there are no recent updates or app lock is on ([@AntsyLich](https://github.com/AntsyLich)) ([`76b146f`](https://github.com/mihonapp/mihon/commit/76b146f))
|
||||
- Fixed a crash when range selecting library entries across categories ([@AntsyLich](https://github.com/AntsyLich)) ([`a6ee4bc`](https://github.com/mihonapp/mihon/commit/a6ee4bc))
|
||||
- Fixed reader tap navigation not responding after a button press turned into a scroll ([@mujina3](https://github.com/mujina3)) ([#3655](https://github.com/mihonapp/mihon/pull/3655))
|
||||
- Fixed editing the cover of an entry that isn't in the library saying the cover was updated without changing it, and say to add it to the library first instead ([@quickdesh](https://github.com/quickdesh), [@AntsyLich](https://github.com/AntsyLich)) ([#2265](https://github.com/mihonapp/mihon/pull/2265))
|
||||
- Fixed downloads of duplicate chapters not being deleted when they were marked as read along with a chapter ([@leodyversemilla07](https://github.com/leodyversemilla07), [@AntsyLich](https://github.com/AntsyLich)) ([#3030](https://github.com/mihonapp/mihon/pull/3030))
|
||||
- Fixed library updates skipping entries whose details were never fetched ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#3179](https://github.com/mihonapp/mihon/pull/3179))
|
||||
- Fixed every library entry being matched as a duplicate of an entry with a blank title ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#3308](https://github.com/mihonapp/mihon/pull/3308))
|
||||
- Fixed marking chapters as read deleting their downloads in categories excluded from "Delete after reading" ([@mliem2k](https://github.com/mliem2k)) ([#3558](https://github.com/mihonapp/mihon/pull/3558))
|
||||
- Fixed local source entries missing from global search ([@mugetau](https://github.com/mugetau)) ([#3784](https://github.com/mihonapp/mihon/pull/3784))
|
||||
- Fixed the Updates screen hiding the last update time and pull to refresh when it has no chapters, and saying there are no recent updates when filters hide them all ([@AMendoza180890](https://github.com/AMendoza180890), [@AntsyLich](https://github.com/AntsyLich)) ([#3790](https://github.com/mihonapp/mihon/pull/3790))
|
||||
- Fixed downloads of chapters removed by the source getting stuck in the download queue ([@JoJoKorok](https://github.com/JoJoKorok), [@AntsyLich](https://github.com/AntsyLich)) ([#3845](https://github.com/mihonapp/mihon/pull/3845))
|
||||
- Fixed the stored download index overwriting the startup rescan, which could show downloaded chapters as not downloaded ([@choppeh](https://github.com/choppeh), [@AntsyLich](https://github.com/AntsyLich)) ([#3968](https://github.com/mihonapp/mihon/pull/3968))
|
||||
- Fixed local chapters no longer being listed after one archive couldn't be read ([@a-biad](https://github.com/a-biad), [@AntsyLich](https://github.com/AntsyLich)) ([#3997](https://github.com/mihonapp/mihon/pull/3997))
|
||||
- Fixed "Hide missing chapter indicators" not hiding the indicator in chapter headers ([@kaosjammo](https://github.com/kaosjammo)) ([#4039](https://github.com/mihonapp/mihon/pull/4039))
|
||||
- Fixed a chapter being downloaded a second time when downloads were stopped right as it finished ([@AntsyLich](https://github.com/AntsyLich)) ([`6115783`](https://github.com/mihonapp/mihon/commit/6115783))
|
||||
- Fixed the chapter selection being cleared when new chapters were downloaded automatically after a refresh ([@AntsyLich](https://github.com/AntsyLich)) ([`a82d27c`](https://github.com/mihonapp/mihon/commit/a82d27c))
|
||||
- Fixed actions started from a dialog sometimes not running when the dialog closed right away, Kitsu, Shikimori, MangaBaka, Bangumi and Hikka sometimes refreshing their login twice at once, adding an extension repo served over HTTP failing, and cancelling an extension install not stopping its download ([@AntsyLich](https://github.com/AntsyLich)) ([#4068](https://github.com/mihonapp/mihon/pull/4068))
|
||||
- Fixed the next library update staying "Soon" after new chapters were found, and chapters that came out early, late or were dated earlier by the source pushing the next check back ([@AntsyLich](https://github.com/AntsyLich)) ([`ba44815`](https://github.com/mihonapp/mihon/commit/ba44815))
|
||||
- Fixed long strip side padding being counted in zoom bounds ([@WizaHX](https://github.com/WizaHX)) ([#4072](https://github.com/mihonapp/mihon/pull/4072))
|
||||
- Fixed long strip stuttering between pages and using a lot of memory with very tall pages ([@AntsyLich](https://github.com/AntsyLich), [@matheuswr89](https://github.com/matheuswr89))
|
||||
|
||||
### Other
|
||||
- Show "Nightly" instead of "Beta" for preview builds and "FOSS" for FOSS builds in About ([@AntsyLich](https://github.com/AntsyLich)) ([#3760](https://github.com/mihonapp/mihon/pull/3760))
|
||||
- Migrate Kitsu tracker to Kitsu's GraphQL API ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||
- Fixed Kitsu search showing novel results ([@MajorTanya](https://github.com/MajorTanya)) ([#4069](https://github.com/mihonapp/mihon/pull/4069))
|
||||
- Extensions can only get Application, Context, Json, ProtoBuf, XML, NetworkHelper and JavaScriptEngine from Injekt ([@AntsyLich](https://github.com/AntsyLich)) ([#3965](https://github.com/mihonapp/mihon/pull/3965))
|
||||
- Use Apollo GraphQL for AniList, Kitsu, Shikimori and Suwayomi tracker APIs ([@MajorTanya](https://github.com/MajorTanya), [@Syer10](https://github.com/Syer10)) ([#3827](https://github.com/mihonapp/mihon/pull/3827))
|
||||
|
||||
## [v0.20.4] - 2026-08-05
|
||||
### Fixed
|
||||
- Fixed some dates being wrongly shown as "Today" or otherwise closer in time ([@MajorTanya](https://github.com/MajorTanya)) ([#3696](https://github.com/mihonapp/mihon/pull/3696))
|
||||
|
||||
## [v0.20.3] - 2026-08-04
|
||||
### Added
|
||||
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
||||
- Add Category filtering for the Upcoming calendar ([@MajorTanya](https://github.com/MajorTanya)) ([#3607](https://github.com/mihonapp/mihon/pull/3607))
|
||||
|
||||
### Changed
|
||||
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Improved
|
||||
- Spoof `Sec-CH-UA` client hints in WebView to match the user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Fixed
|
||||
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676))
|
||||
- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
- Fixed reader loading indefinitely in some scenarios ([@AntsyLich](https://github.com/AntsyLich)) ([#3686](https://github.com/mihonapp/mihon/pull/3686))
|
||||
- Fix MyAnimeList error for list entries with partial start/finish reading dates by setting those dates to either Jan 1st or 1st of the month ([@MajorTanya](https://github.com/MajorTanya)) ([#3573](https://github.com/mihonapp/mihon/pull/3573))
|
||||
|
||||
## [v0.20.2] - 2026-08-01
|
||||
### Added
|
||||
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||
- Fix HTTP Error 416 not being handled properly ([@AntsyLich](https://github.com/AntsyLich)) ([#3563](https://github.com/mihonapp/mihon/pull/3563))
|
||||
- Fix incorrect progress when resuming downloads ([@xMohnad](https://github.com/xMohnad)) ([#3616](https://github.com/mihonapp/mihon/pull/3616))
|
||||
- Add Tokyo Night Theme ([@xMohnad](https://github.com/xMohnad)) ([#3502](https://github.com/mihonapp/mihon/pull/3502))
|
||||
|
||||
### Changed
|
||||
- Detect Shizuku with permission check ([@Small-Ku](https://github.com/Small-Ku)) ([#3565](https://github.com/mihonapp/mihon/pull/3565))
|
||||
- Check for app and extension update on every cold start ([@AntsyLich](https://github.com/AntsyLich)) ([#3658](https://github.com/mihonapp/mihon/pull/3658))
|
||||
|
||||
### Improved
|
||||
- Library search ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3554](https://github.com/mihonapp/mihon/pull/3554))
|
||||
- Support logical operators (`&&`, `||`, `-`), implicit `AND` separators, and nested expressions with `()`
|
||||
- Add comparison operators (`>`, `>=`, `<`, `<=`, `=`) for `id`, `added`, `fetchinterval` / `fi`, `nextupdate` / `nu`, `unread`, `read`, and `total` (Uses `yyyy-MM-dd` dates for `added` and `nextupdate`)
|
||||
- Support field-specific prefixes (`title`, `author`, `artist`, `description` / `desc`, `genre` / `tag`, `source` / `src`, `source_id` / `src_id`, `notes` / `note`, `language` / `lang`) and empty field matching (`""`, `''`)
|
||||
- Always use Private installer to update privately installed extensions ([@MajorTanya](https://github.com/MajorTanya)) ([#3614](https://github.com/mihonapp/mihon/pull/3614))
|
||||
- App update prompt flow experience ([@AntsyLich](https://github.com/AntsyLich)) ([#3669](https://github.com/mihonapp/mihon/pull/3669))
|
||||
|
||||
### Fixed
|
||||
- Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider ([@AntsyLich](https://github.com/AntsyLich)) ([#3567](https://github.com/mihonapp/mihon/pull/3567))
|
||||
- Fixed MangaBaka User Agent string ([@MajorTanya](https://github.com/MajorTanya)) ([#3578](https://github.com/mihonapp/mihon/pull/3578))
|
||||
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3630](https://github.com/mihonapp/mihon/pull/3630))
|
||||
- Fixed backup restore dropping library entries when the backup contains duplicate chapters ([@na-ji](https://github.com/na-ji)) ([#3667](https://github.com/mihonapp/mihon/pull/3667))
|
||||
|
||||
## [v0.20.1] - 2026-07-09
|
||||
### Added
|
||||
@@ -467,7 +660,7 @@ Same as v0.19.6
|
||||
- Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
||||
- Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
||||
- Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef))
|
||||
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
|
||||
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1114](https://github.com/mihonapp/mihon/pull/1114))
|
||||
- Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605))
|
||||
- Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a))
|
||||
- Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9))
|
||||
@@ -579,7 +772,12 @@ Same as v0.19.6
|
||||
- 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.20.1...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.21.1...main
|
||||
[v0.21.1]: https://github.com/mihonapp/mihon/compare/v0.21.0...v0.21.1
|
||||
[v0.21.0]: https://github.com/mihonapp/mihon/compare/v0.20.4...v0.21.0
|
||||
[v0.20.4]: https://github.com/mihonapp/mihon/compare/v0.20.3...v0.20.4
|
||||
[v0.20.3]: https://github.com/mihonapp/mihon/compare/v0.20.2...v0.20.3
|
||||
[v0.20.2]: https://github.com/mihonapp/mihon/compare/v0.20.1...v0.20.2
|
||||
[v0.20.1]: https://github.com/mihonapp/mihon/compare/v0.20.0...v0.20.1
|
||||
[v0.20.0]: https://github.com/mihonapp/mihon/compare/v0.19.9...v0.20.0
|
||||
[v0.19.9]: https://github.com/mihonapp/mihon/compare/v0.19.8...v0.19.9
|
||||
|
||||
@@ -29,7 +29,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
|
||||
* Local reading of content.
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/) and [Hikka](https://hikka.io/) support.
|
||||
* Tracker support: [MangaBaka](https://mangabaka.org), [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/), and [Hikka](https://hikka.io/) support.
|
||||
* Categories to organize your library.
|
||||
* Light and dark themes.
|
||||
* Schedule updating your library for new chapters.
|
||||
@@ -49,8 +49,8 @@ Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/fa
|
||||
|
||||
### Repositories
|
||||
|
||||
[](https://github.com/mihonapp/website/)
|
||||
[](https://github.com/mihonapp/bitmap.kt/)
|
||||
[](https://github.com/mihonapp/website/)
|
||||
[](https://github.com/mihonapp/bitmap.kt/)
|
||||
|
||||
### Credits
|
||||
|
||||
|
||||
+128
-27
@@ -1,20 +1,25 @@
|
||||
import com.android.build.api.variant.ApplicationVariant
|
||||
import com.android.build.api.variant.BuildConfigField
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||
import mihon.gradle.Config
|
||||
import mihon.gradle.getBuildTime
|
||||
import mihon.gradle.getCurrentTime
|
||||
import mihon.gradle.getLatestCommitCount
|
||||
import mihon.gradle.getLatestCommitSha
|
||||
import mihon.gradle.getLatestCommitTime
|
||||
import mihon.gradle.tasks.ReplaceShortcutsPlaceholderTask
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
plugins {
|
||||
alias(mihonx.plugins.android.application)
|
||||
alias(mihonx.plugins.compose)
|
||||
alias(mihonx.plugins.spotless)
|
||||
|
||||
alias(libs.plugins.metro)
|
||||
alias(libs.plugins.aboutLibraries)
|
||||
alias(libs.plugins.androidx.baselineProfile)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.apollo)
|
||||
}
|
||||
|
||||
if (Config.includeTelemetry) {
|
||||
@@ -24,7 +29,7 @@ if (Config.includeTelemetry) {
|
||||
}
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystorePropertiesFile = layout.settingsDirectory.file("keystore.properties").asFile
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
@@ -32,12 +37,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 26
|
||||
versionName = "0.20.1"
|
||||
versionCode = 37
|
||||
versionName = "0.21.1"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = false)}\"")
|
||||
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
||||
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
||||
|
||||
@@ -45,14 +47,9 @@ android {
|
||||
}
|
||||
|
||||
if (System.getenv("MIHON_GITHUB_RELEASE").toBoolean()) {
|
||||
val tempStoreFile = file(System.getenv("RUNNER_TEMP")).resolve("antsy.keystore")
|
||||
|
||||
val storeFileBytes = System.getenv("storeFileBase64").let(Base64::decode)
|
||||
tempStoreFile.outputStream().use { it.write(storeFileBytes) }
|
||||
|
||||
signingConfigs {
|
||||
named("debug") {
|
||||
storeFile = tempStoreFile
|
||||
storeFile = file(System.getenv("storeFile"))
|
||||
storePassword = System.getenv("storePassword")
|
||||
keyAlias = System.getenv("keyAlias")
|
||||
keyPassword = System.getenv("keyPassword")
|
||||
@@ -74,7 +71,6 @@ android {
|
||||
buildTypes {
|
||||
val debug = getByName("debug") {
|
||||
applicationIdSuffix = ".dev"
|
||||
versionNameSuffix = "-${getLatestCommitCount()}"
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
val release = getByName("release") {
|
||||
@@ -86,8 +82,6 @@ android {
|
||||
isProfileable = true
|
||||
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = true)}\"")
|
||||
}
|
||||
|
||||
val commonMatchingFallbacks = listOf(release.name)
|
||||
@@ -99,16 +93,12 @@ android {
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
create("preview") {
|
||||
create("nightly") {
|
||||
initWith(release)
|
||||
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
versionNameSuffix = debug.versionNameSuffix
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLatestCommitTime = false)}\"")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(release)
|
||||
@@ -118,10 +108,18 @@ android {
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
|
||||
if (Config.includeTelemetry) {
|
||||
configureEach {
|
||||
configure<CrashlyticsExtension> {
|
||||
mappingFileUploadEnabled = Config.uploadCrashlyticsMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("preview").res.directories.add("src/debug/res")
|
||||
getByName("nightly").res.directories.add("src/debug/res")
|
||||
getByName("benchmark").res.directories.add("src/debug/res")
|
||||
}
|
||||
|
||||
@@ -207,8 +205,12 @@ dependencies {
|
||||
baselineProfile(projects.baselineProfile)
|
||||
|
||||
implementation(projects.i18n)
|
||||
implementation(projects.icons.customMaterialSymbols)
|
||||
implementation(projects.icons.materialSymbols)
|
||||
implementation(projects.icons.simpleIcons)
|
||||
implementation(projects.core.archive)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.metro)
|
||||
implementation(projects.coreMetadata)
|
||||
implementation(projects.sourceApi)
|
||||
implementation(projects.sourceLocal)
|
||||
@@ -222,7 +224,6 @@ dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.materialIcons)
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.animationGraphics)
|
||||
debugImplementation(libs.androidx.compose.uiTooling)
|
||||
@@ -234,13 +235,11 @@ dependencies {
|
||||
implementation(libs.androidx.paging.runtime)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
implementation(libs.bundles.kotlinx.coroutines)
|
||||
|
||||
implementation(libs.sqldelight.async)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation(libs.androidx.annotation)
|
||||
@@ -281,6 +280,9 @@ dependencies {
|
||||
|
||||
// Dependency injection
|
||||
implementation(libs.injekt)
|
||||
implementation(libs.metro.runtime)
|
||||
implementation(libs.metrox.viewmodel)
|
||||
implementation(libs.metrox.viewmodel.compose)
|
||||
|
||||
// Image loading
|
||||
implementation(libs.bundles.coil)
|
||||
@@ -316,6 +318,10 @@ dependencies {
|
||||
// String similarity
|
||||
implementation(libs.stringSimilarity)
|
||||
|
||||
// GraphQL generation
|
||||
implementation(libs.apollo)
|
||||
implementation(libs.apollo.adapters)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
@@ -327,7 +333,102 @@ dependencies {
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
||||
apollo {
|
||||
val schemaBasePath = "src/main/graphql/mihon/graphql"
|
||||
service("anilist") {
|
||||
packageName.set("mihon.graphql.anilist")
|
||||
val srcDir = "$schemaBasePath/anilist"
|
||||
srcDir(file(srcDir))
|
||||
|
||||
introspection {
|
||||
endpointUrl.set("https://graphql.anilist.co")
|
||||
schemaFile.set(file("$srcDir/anilist.graphqls"))
|
||||
}
|
||||
|
||||
// ISO 3166-1 alpha-2 country code
|
||||
mapScalarToKotlinString("CountryCode")
|
||||
}
|
||||
|
||||
service("kitsu") {
|
||||
packageName.set("mihon.graphql.kitsu")
|
||||
val srcDir = "$schemaBasePath/kitsu"
|
||||
srcDir(file(srcDir))
|
||||
android.defaultConfig.versionCode
|
||||
|
||||
introspection {
|
||||
// Kitsu doesn't like requests without a UA
|
||||
headers.put(
|
||||
"User-Agent",
|
||||
"Mihon v${android.defaultConfig.versionName} (${android.defaultConfig.applicationId})",
|
||||
)
|
||||
endpointUrl.set("https://kitsu.app/api/graphql")
|
||||
schemaFile.set(file("$srcDir/kitsu.graphqls"))
|
||||
}
|
||||
|
||||
// A date, expressed as an ISO8601 string
|
||||
mapScalarToKotlinString("Date")
|
||||
// An ISO 8601-encoded datetime
|
||||
mapScalar("ISO8601DateTime", "kotlin.time.Instant", "com.apollographql.adapter.core.KotlinInstantAdapter")
|
||||
// A loose key-value map in GraphQL
|
||||
mapScalar(
|
||||
"Map",
|
||||
"kotlin.collections.Map<String, Any?>",
|
||||
"mihon.graphql.kitsu.adapter.KitsuMapAdapter",
|
||||
)
|
||||
}
|
||||
|
||||
service("shikimori") {
|
||||
packageName.set("mihon.graphql.shikimori")
|
||||
val srcDir = "$schemaBasePath/shikimori"
|
||||
srcDir(file(srcDir))
|
||||
|
||||
introspection {
|
||||
endpointUrl.set("https://shikimori.io/api/graphql")
|
||||
schemaFile.set(file("$srcDir/shikimori.graphqls"))
|
||||
}
|
||||
|
||||
// An ISO 8601-encoded date
|
||||
mapScalarToKotlinString("ISO8601Date")
|
||||
}
|
||||
|
||||
service("suwayomi") {
|
||||
packageName.set("mihon.graphql.suwayomi")
|
||||
val srcDir = "$schemaBasePath/suwayomi"
|
||||
srcDir(file(srcDir))
|
||||
|
||||
introspection {
|
||||
endpointUrl.set("http://localhost:4567/api/graphql")
|
||||
schemaFile.set(file("$srcDir/suwayomi.graphqls"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val latestCommitCount = getLatestCommitCount()
|
||||
val latestCommitSha = getLatestCommitSha()
|
||||
val latestCommitTime = getLatestCommitTime()
|
||||
val currentTime = getCurrentTime()
|
||||
|
||||
fun ApplicationVariant.buildConfigField(type: String, name: String, value: Provider<String>) {
|
||||
buildConfigFields?.put(name, value.map { BuildConfigField(type, it, null) })
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val isUnstableBuild = variant.buildType == "debug" || variant.buildType == "nightly"
|
||||
val buildTime = if (isUnstableBuild) currentTime else latestCommitTime
|
||||
|
||||
variant.buildConfigField("String", "COMMIT_COUNT", latestCommitCount.map { "\"$it\"" })
|
||||
variant.buildConfigField("String", "COMMIT_SHA", latestCommitSha.map { "\"$it\"" })
|
||||
variant.buildConfigField("String", "BUILD_TIME", buildTime.map { "\"$it\"" })
|
||||
|
||||
if (isUnstableBuild) {
|
||||
variant.outputs.forEach { output ->
|
||||
val versionName = output.versionName.get()
|
||||
output.versionName.set(latestCommitCount.map { "$versionName-$it" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVariants { variant ->
|
||||
val resSource = variant.sources.res ?: return@onVariants
|
||||
|
||||
|
||||
Vendored
+16
-11
@@ -1,8 +1,8 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keep,allowoptimization class eu.kanade.**
|
||||
-keep,allowoptimization class tachiyomi.**
|
||||
-keep,allowoptimization class mihon.**
|
||||
# Injekt reads the injected type from the generic superclass of its type tokens
|
||||
-keepattributes Signature
|
||||
-keep,allowshrinking class * extends uy.kohesive.injekt.api.FullTypeReference
|
||||
|
||||
# Keep common dependencies used in extensions
|
||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||
@@ -18,19 +18,15 @@
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
-keep,allowoptimization class com.squareup.zstd.** { public protected *; }
|
||||
|
||||
# From extensions-lib
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# Provided by the OEM at runtime when supported
|
||||
-dontwarn androidx.window.extensions.**
|
||||
-dontwarn androidx.window.sidecar.**
|
||||
|
||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
@@ -86,3 +82,12 @@
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
||||
# KotlinX Datetime
|
||||
-keep,allowoptimization class kotlinx.datetime.** { public protected *; }
|
||||
|
||||
# Methods called by Shizuku only
|
||||
-keepclassmembers class mihon.app.shizuku.ShellInterface {
|
||||
public <init>();
|
||||
public void destroy();
|
||||
}
|
||||
|
||||
@@ -198,6 +198,17 @@
|
||||
<data android:host="shikimori-auth" />
|
||||
<data android:host="hikka-auth" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- Comick requires native reverse-domain schemes -->
|
||||
<data android:scheme="app.mihon" />
|
||||
<data android:host="comick-auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package mihon.app.shizuku;
|
||||
|
||||
interface IShellInterface {
|
||||
void install(in AssetFileDescriptor apk) = 1;
|
||||
void install(in AssetFileDescriptor apk, in IntentSender intentSender) = 1;
|
||||
|
||||
void destroy() = 16777114;
|
||||
}
|
||||
|
||||
+11882
-14172
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
mutation AniListAddManga(
|
||||
$manga_id: Int!,
|
||||
$progress: Int!,
|
||||
$status: MediaListStatus!,
|
||||
$private: Boolean!,
|
||||
) {
|
||||
SaveMediaListEntry (
|
||||
mediaId: $manga_id,
|
||||
progress: $progress,
|
||||
status: $status,
|
||||
private: $private,
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mutation AniListDeleteManga(
|
||||
$library_id: Int!,
|
||||
) {
|
||||
DeleteMediaListEntry(
|
||||
id: $library_id,
|
||||
) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query AniListGetCurrentUser {
|
||||
Viewer {
|
||||
id
|
||||
name
|
||||
mediaListOptions {
|
||||
scoreFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
query AniListGetLibManga(
|
||||
$user_id: Int!,
|
||||
$manga_id: Int!,
|
||||
) {
|
||||
Page {
|
||||
mediaList(
|
||||
userId: $user_id,
|
||||
type: MANGA,
|
||||
mediaId: $manga_id,
|
||||
) {
|
||||
id
|
||||
status
|
||||
scoreRaw: score(format: POINT_100)
|
||||
progress
|
||||
private
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
media {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query AniListGetMangaDetails(
|
||||
$manga_id: Int!,
|
||||
) {
|
||||
Page (perPage: 1) {
|
||||
media(
|
||||
id: $manga_id,
|
||||
type: MANGA,
|
||||
format_not_in: [NOVEL],
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query AniListSearchManga(
|
||||
$search: String!,
|
||||
) {
|
||||
Page (perPage: 50) {
|
||||
media(
|
||||
search: $search,
|
||||
type: MANGA,
|
||||
format_not_in: [NOVEL],
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
mutation AniListUpdateManga(
|
||||
$library_id: Int!,
|
||||
$progress: Int!,
|
||||
$status: MediaListStatus!,
|
||||
$private: Boolean!,
|
||||
$score: Int!,
|
||||
$startedAt: FuzzyDateInput!,
|
||||
$completedAt: FuzzyDateInput!,
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
id: $library_id,
|
||||
progress: $progress,
|
||||
status: $status,
|
||||
private: $private,
|
||||
scoreRaw: $score,
|
||||
startedAt: $startedAt,
|
||||
completedAt: $completedAt,
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
fragment MangaFragment on Media {
|
||||
id
|
||||
staff {
|
||||
edges {
|
||||
role
|
||||
node {
|
||||
name {
|
||||
userPreferred
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
format
|
||||
countryOfOrigin
|
||||
status
|
||||
chapters
|
||||
description
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
averageScore
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
mutation KitsuAddLibManga(
|
||||
$media_id: ID!,
|
||||
$status: LibraryEntryStatusEnum!,
|
||||
$progress: Int!,
|
||||
$private: Boolean!,
|
||||
$rating: Int,
|
||||
) {
|
||||
libraryEntry {
|
||||
create(
|
||||
input: {
|
||||
mediaId: $media_id,
|
||||
mediaType: MANGA,
|
||||
status: $status,
|
||||
progress: $progress,
|
||||
private: $private,
|
||||
rating: $rating,
|
||||
}
|
||||
) {
|
||||
errors {
|
||||
message
|
||||
}
|
||||
libraryEntry {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
mutation KitsuDeleteLibEntry(
|
||||
$library_id: ID!,
|
||||
) {
|
||||
libraryEntry {
|
||||
delete(
|
||||
input: {
|
||||
id: $library_id,
|
||||
}
|
||||
) {
|
||||
errors {
|
||||
message
|
||||
}
|
||||
libraryEntry {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
query KitsuFindLibManga(
|
||||
$remote_id: ID!,
|
||||
) {
|
||||
findMangaById(
|
||||
id: $remote_id,
|
||||
) {
|
||||
...MangaFragment
|
||||
myLibraryEntry {
|
||||
id
|
||||
private
|
||||
progress
|
||||
rating
|
||||
status
|
||||
startedAt
|
||||
finishedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query KitsuGetCurrentAccount {
|
||||
currentAccount {
|
||||
id
|
||||
ratingSystem
|
||||
profile {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query KitsuGetMangaDetailsById(
|
||||
$id: ID!,
|
||||
) {
|
||||
findMangaById(
|
||||
id: $id,
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query KitsuGetMangaDetailsBySlug(
|
||||
$slug: String!,
|
||||
) {
|
||||
findMangaBySlug(
|
||||
slug: $slug,
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query KitsuSearchMangaByTitle(
|
||||
$query: String!,
|
||||
) {
|
||||
searchMangaByTitle(
|
||||
title: $query,
|
||||
first: 20,
|
||||
) {
|
||||
nodes {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
mutation KitsuUpdateLibManga(
|
||||
$library_id: ID!,
|
||||
$status: LibraryEntryStatusEnum!,
|
||||
$progress: Int!,
|
||||
$private: Boolean!,
|
||||
$rating: Int,
|
||||
$startedAt: ISO8601DateTime,
|
||||
$finishedAt: ISO8601DateTime,
|
||||
) {
|
||||
libraryEntry {
|
||||
update(
|
||||
input: {
|
||||
id: $library_id,
|
||||
status: $status,
|
||||
progress: $progress,
|
||||
private: $private,
|
||||
rating: $rating,
|
||||
startedAt: $startedAt,
|
||||
finishedAt: $finishedAt,
|
||||
}
|
||||
) {
|
||||
errors {
|
||||
message
|
||||
}
|
||||
libraryEntry {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
fragment MangaFragment on Manga {
|
||||
id
|
||||
titles {
|
||||
preferred
|
||||
}
|
||||
chapterCount
|
||||
staff(first: 5) {
|
||||
nodes {
|
||||
role
|
||||
person {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
posterImage {
|
||||
views(names: "small") {
|
||||
name
|
||||
url
|
||||
}
|
||||
original {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
description(locales: "en")
|
||||
status
|
||||
subtype
|
||||
startDate
|
||||
slug
|
||||
averageRating
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
fragment MangaFragment on Manga {
|
||||
id
|
||||
name
|
||||
chapters
|
||||
kind
|
||||
poster {
|
||||
mainUrl
|
||||
}
|
||||
score
|
||||
url
|
||||
status
|
||||
airedOn {
|
||||
date
|
||||
}
|
||||
description
|
||||
personRoles {
|
||||
person {
|
||||
name
|
||||
}
|
||||
rolesEn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
query ShikimoriGetCurrentUser {
|
||||
currentUser {
|
||||
id
|
||||
nickname
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
query ShikimoriGetLibManga(
|
||||
$remote_id: String!,
|
||||
) {
|
||||
mangas(
|
||||
ids: $remote_id,
|
||||
limit: 1,
|
||||
) {
|
||||
id
|
||||
url
|
||||
name
|
||||
chapters
|
||||
userRate {
|
||||
id
|
||||
chapters
|
||||
status
|
||||
score
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
query ShikimoriGetMangaDetails(
|
||||
$query: String!,
|
||||
) {
|
||||
mangas(
|
||||
ids: $query,
|
||||
limit: 1,
|
||||
kind: "!light_novel,!novel",
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
query ShikimoriSearchManga(
|
||||
$search: String!
|
||||
) {
|
||||
mangas(
|
||||
search: $search,
|
||||
limit: 20,
|
||||
kind: "!light_novel,!novel",
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
fragment MangaFragment on MangaType {
|
||||
description
|
||||
id
|
||||
status
|
||||
thumbnailUrl
|
||||
title
|
||||
url
|
||||
chapters {
|
||||
totalCount
|
||||
}
|
||||
latestReadChapter {
|
||||
chapterNumber
|
||||
}
|
||||
unreadCount
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query SuwayomiGetManga(
|
||||
$mangaId: Int!,
|
||||
) {
|
||||
manga(
|
||||
id: $mangaId,
|
||||
) {
|
||||
...MangaFragment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
query SuwayomiGetMangaUnreadChapters(
|
||||
$mangaId: Int!,
|
||||
$chapterNumber: Float!,
|
||||
) {
|
||||
chapters(
|
||||
condition: {
|
||||
mangaId: $mangaId,
|
||||
isRead: false,
|
||||
},
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
chapterNumber: {
|
||||
lessThanOrEqualTo: $chapterNumber
|
||||
}
|
||||
},
|
||||
{
|
||||
chapterNumber: {
|
||||
greaterThan: 0
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
){
|
||||
nodes {
|
||||
id
|
||||
chapterNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
mutation SuwayomiMarkChaptersRead(
|
||||
$chapters: [Int!]!,
|
||||
) {
|
||||
updateChapters(
|
||||
input: {
|
||||
ids: $chapters,
|
||||
patch: {
|
||||
isRead: true,
|
||||
},
|
||||
},
|
||||
) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
mutation SuwayomiMarkAndDeleteChapters(
|
||||
$chapters: [Int!]!,
|
||||
) {
|
||||
updateChapters(
|
||||
input: {
|
||||
ids: $chapters,
|
||||
patch: {
|
||||
isRead: true,
|
||||
},
|
||||
},
|
||||
) {
|
||||
__typename
|
||||
}
|
||||
|
||||
deleteDownloadedChapters(
|
||||
input: {
|
||||
ids: $chapters,
|
||||
},
|
||||
) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mutation SuwayomiUpdateMangaProgress(
|
||||
$mangaId: Int!,
|
||||
) {
|
||||
trackProgress(
|
||||
input: {
|
||||
mangaId: $mangaId,
|
||||
},
|
||||
) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun ifSourcesLoaded(): Boolean {
|
||||
return remember { Injekt.get<SourceManager>().isInitialized }.collectAsState().value
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package eu.kanade.domain
|
||||
|
||||
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetIncognitoState
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleIncognito
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import mihon.data.extension.repository.ExtensionStoreRepositoryImpl
|
||||
import mihon.data.extension.service.ExtensionStoreService
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import mihon.domain.extension.interactor.AddExtensionStore
|
||||
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||
import mihon.domain.source.interactor.UpdateMangaFromRemote
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||
import tachiyomi.data.release.ReleaseServiceImpl
|
||||
import tachiyomi.data.source.SourceRepositoryImpl
|
||||
import tachiyomi.data.source.StubSourceRepositoryImpl
|
||||
import tachiyomi.data.track.TrackRepositoryImpl
|
||||
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
||||
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
||||
import tachiyomi.domain.category.interactor.DeleteCategory
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.RenameCategory
|
||||
import tachiyomi.domain.category.interactor.ReorderCategory
|
||||
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
||||
import tachiyomi.domain.category.interactor.SetDisplayMode
|
||||
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
|
||||
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||
import tachiyomi.domain.release.service.ReleaseService
|
||||
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||
import tachiyomi.domain.source.repository.SourceRepository
|
||||
import tachiyomi.domain.source.repository.StubSourceRepository
|
||||
import tachiyomi.domain.track.interactor.DeleteTrack
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.interactor.GetTracksPerManga
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DomainModule : InjektModule {
|
||||
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
||||
addFactory { GetCategories(get()) }
|
||||
addFactory { ResetCategoryFlags(get(), get()) }
|
||||
addFactory { SetDisplayMode(get()) }
|
||||
addFactory { SetSortModeForCategory(get(), get()) }
|
||||
addFactory { CreateCategoryWithName(get(), get()) }
|
||||
addFactory { RenameCategory(get()) }
|
||||
addFactory { ReorderCategory(get()) }
|
||||
addFactory { UpdateCategory(get()) }
|
||||
addFactory { DeleteCategory(get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
addFactory { GetDuplicateLibraryManga(get()) }
|
||||
addFactory { GetFavorites(get()) }
|
||||
addFactory { GetLibraryManga(get()) }
|
||||
addFactory { GetMangaWithChapters(get(), get()) }
|
||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
||||
addFactory { GetManga(get()) }
|
||||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { GetUpcomingManga(get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { FetchInterval(get()) }
|
||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get(), get()) }
|
||||
addFactory { UpdateMangaNotes(get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
addFactory { GetExcludedScanlators(get()) }
|
||||
addFactory { SetExcludedScanlators(get()) }
|
||||
addFactory {
|
||||
MigrateMangaUseCase(
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
addFactory { GetApplicationRelease(get(), get()) }
|
||||
|
||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||
addFactory { TrackChapter(get(), get(), get(), get()) }
|
||||
addFactory { AddTracks(get(), get(), get(), get()) }
|
||||
addFactory { RefreshTracks(get(), get(), get(), get()) }
|
||||
addFactory { DeleteTrack(get()) }
|
||||
addFactory { GetTracksPerManga(get()) }
|
||||
addFactory { GetTracks(get()) }
|
||||
addFactory { InsertTrack(get()) }
|
||||
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
|
||||
|
||||
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()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
addFactory { FilterChaptersForDownload(get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
addFactory { UpsertHistory(get()) }
|
||||
addFactory { RemoveHistory(get()) }
|
||||
addFactory { GetTotalReadDuration(get()) }
|
||||
|
||||
addFactory { DeleteDownload(get(), get()) }
|
||||
|
||||
addFactory { GetExtensionsByType(get(), get()) }
|
||||
addFactory { GetExtensionSources(get()) }
|
||||
addFactory { GetExtensionLanguages(get(), get()) }
|
||||
|
||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||
addFactory { GetUpdates(get()) }
|
||||
|
||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
||||
addFactory { GetEnabledSources(get(), get()) }
|
||||
addFactory { GetLanguagesWithSources(get(), get()) }
|
||||
addFactory { GetRemoteManga(get()) }
|
||||
addFactory { GetSourcesWithFavoriteCount(get(), get()) }
|
||||
addFactory { GetSourcesWithNonLibraryManga(get()) }
|
||||
addFactory { SetMigrateSorting(get()) }
|
||||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleSource(get()) }
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
addFactory { TrustExtension(get(), get()) }
|
||||
|
||||
addSingletonFactory { ExtensionStoreService(get(), get(), get()) }
|
||||
addSingletonFactory<ExtensionStoreRepository> { ExtensionStoreRepositoryImpl(get(), get()) }
|
||||
addFactory { AddExtensionStore(get()) }
|
||||
addFactory { GetExtensionStoreCountAsFlow(get()) }
|
||||
addFactory { GetExtensionStores(get()) }
|
||||
addFactory { RemoveExtensionStore(get()) }
|
||||
addFactory { UpdateExtensionStores(get()) }
|
||||
|
||||
addFactory { ToggleIncognito(get()) }
|
||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||
|
||||
addFactory { UpdateMangaFromRemote(get(), get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@ package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
preferenceStore: PreferenceStore,
|
||||
@@ -40,15 +45,15 @@ class BasePreferences(
|
||||
GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
)
|
||||
|
||||
val alwaysDecodeLongStripWithSSIV: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"pref_always_decode_long_strip_with_ssiv",
|
||||
false,
|
||||
)
|
||||
|
||||
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||
|
||||
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
Preference.appStateKey("donation_campaign_shown"),
|
||||
false,
|
||||
)
|
||||
|
||||
val skippedAppUpdate: Preference<String> = preferenceStore.getString(
|
||||
Preference.appStateKey("skipped_app_update"),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
@Inject
|
||||
class GetAvailableScanlators(
|
||||
private val repository: ChapterRepository,
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
@@ -11,6 +11,7 @@ import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@Inject
|
||||
class SetReadStatus(
|
||||
private val downloadPreferences: DownloadPreferences,
|
||||
private val deleteDownload: DeleteDownload,
|
||||
@@ -19,14 +20,13 @@ class SetReadStatus(
|
||||
) {
|
||||
|
||||
private val mapper = { chapter: Chapter, read: Boolean ->
|
||||
ChapterUpdate(
|
||||
read = read,
|
||||
lastPageRead = if (!read) 0 else null,
|
||||
id = chapter.id,
|
||||
)
|
||||
ChapterUpdate(chapter.id) {
|
||||
this.read = read
|
||||
if (!read) lastPageRead = 0
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
|
||||
suspend fun await(read: Boolean, vararg chapters: Chapter): Result {
|
||||
val chaptersToUpdate = chapters.filter {
|
||||
when (read) {
|
||||
true -> !it.read
|
||||
@@ -34,7 +34,7 @@ class SetReadStatus(
|
||||
}
|
||||
}
|
||||
if (chaptersToUpdate.isEmpty()) {
|
||||
return@withNonCancellableContext Result.NoChapters
|
||||
return Result.NoChapters
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -43,11 +43,14 @@ class SetReadStatus(
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
return Result.InternalError(e)
|
||||
}
|
||||
|
||||
if (read && downloadPreferences.removeAfterMarkedAsRead.get()) {
|
||||
chaptersToUpdate
|
||||
// Chapters here still carry their pre-update read state; downstream
|
||||
// deletion filtering (excluded categories) relies on the new state.
|
||||
.map { it.copy(read = true) }
|
||||
.groupBy { it.mangaId }
|
||||
.forEach { (mangaId, chapters) ->
|
||||
deleteDownload.awaitAll(
|
||||
@@ -57,17 +60,16 @@ class SetReadStatus(
|
||||
}
|
||||
}
|
||||
|
||||
Result.Success
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext {
|
||||
suspend fun await(mangaId: Long, read: Boolean): Result =
|
||||
await(
|
||||
read = read,
|
||||
chapters = chapterRepository
|
||||
.getChapterByMangaId(mangaId)
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun await(manga: Manga, read: Boolean) =
|
||||
await(manga.id, read)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.chapter.model.copyFromSChapter
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||
@@ -10,30 +11,32 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.data.chapter.ChapterSanitizer
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.ChapterRemoteUpdate
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Inject
|
||||
class SyncChaptersWithSource(
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
||||
private val updateManga: UpdateManga,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
private val getExcludedScanlators: GetExcludedScanlators,
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
) {
|
||||
@@ -51,14 +54,15 @@ class SyncChaptersWithSource(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
manualFetch: Boolean = false,
|
||||
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||
fetchWindow: ClosedRange<Instant>? = null,
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val now = Clock.System.now().toLocalDateTime(timeZone)
|
||||
val nowInstant = now.toInstant(timeZone)
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
@@ -69,43 +73,46 @@ class SyncChaptersWithSource(
|
||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||
val dbChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||
val dbChaptersByUrl = dbChapters.associateBy { it.url }
|
||||
|
||||
val newChapters = mutableListOf<Chapter>()
|
||||
val updatedChapters = mutableListOf<Chapter>()
|
||||
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
}
|
||||
val updatedChapters = mutableListOf<ChapterRemoteUpdate>()
|
||||
val sourceUrls = mutableSetOf<String>()
|
||||
|
||||
// Used to not set upload date of older chapters
|
||||
// to a higher value than newer chapters
|
||||
var maxSeenUploadDate = 0L
|
||||
var maxSeenUploadDate: Instant? = null
|
||||
|
||||
for (sourceChapter in sourceChapters) {
|
||||
var chapter = sourceChapter
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is HttpSource) {
|
||||
// Update metadata from source if necessary.
|
||||
val preparedChapters = if (source is HttpSource) {
|
||||
val sManga = manga.toSManga()
|
||||
sourceChapters.map { chapter ->
|
||||
val sChapter = chapter.toSChapter()
|
||||
@Suppress("DEPRECATION")
|
||||
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||
chapter = chapter.copyFromSChapter(sChapter)
|
||||
source.prepareNewChapter(sChapter, sManga)
|
||||
chapter.copyFromSChapter(sChapter)
|
||||
}
|
||||
} else {
|
||||
sourceChapters
|
||||
}
|
||||
|
||||
// Recognize chapter numbers, from the whole list since names alone don't always tell.
|
||||
val recognizedChapters = preparedChapters
|
||||
.zip(ChapterRecognition.parseChapterNumbers(manga.title, preparedChapters)) { chapter, numbering ->
|
||||
chapter.copy(chapterNumber = numbering.chapter?.let(ChapterRecognition::toDouble) ?: -1.0)
|
||||
}
|
||||
|
||||
// Recognize chapter number for the chapter.
|
||||
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber)
|
||||
chapter = chapter.copy(chapterNumber = chapterNumber)
|
||||
for (chapter in recognizedChapters) {
|
||||
if (!sourceUrls.add(chapter.url)) continue
|
||||
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
val dbChapter = dbChaptersByUrl[chapter.url]
|
||||
|
||||
if (dbChapter == null) {
|
||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
chapter.copy(dateUpload = altDateUpload)
|
||||
val toAddChapter = if (chapter.dateUpload == null) {
|
||||
chapter.copy(dateUpload = maxSeenUploadDate ?: nowInstant)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||
maxSeenUploadDate = listOfNotNull(maxSeenUploadDate, chapter.dateUpload).maxOrNull()
|
||||
chapter
|
||||
}
|
||||
newChapters.add(toAddChapter)
|
||||
@@ -124,27 +131,36 @@ class SyncChaptersWithSource(
|
||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||
}
|
||||
|
||||
var toChangeChapter = dbChapter.copy(
|
||||
name = chapter.name,
|
||||
chapterNumber = chapter.chapterNumber,
|
||||
scanlator = chapter.scanlator,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
memo = chapter.memo,
|
||||
updatedChapters.add(
|
||||
ChapterRemoteUpdate(
|
||||
id = dbChapter.id,
|
||||
name = chapter.name,
|
||||
chapterNumber = chapter.chapterNumber,
|
||||
scanlator = chapter.scanlator,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
dateUpload = chapter.dateUpload,
|
||||
memo = chapter.memo,
|
||||
),
|
||||
)
|
||||
|
||||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
updatedChapters.add(toChangeChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val removedChapters = dbChapters.filterNot { it.url in sourceUrls }
|
||||
|
||||
// The source no longer lists these, so their queued downloads could only fail
|
||||
removedChapters.mapNotNull { downloadManager.getQueuedDownloadOrNull(it.id) }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { downloadManager.cancelQueuedDownloads(it) }
|
||||
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||
val isNextUpdateBeforeWindow = fetchWindow != null &&
|
||||
manga.nextUpdate.let { it == null || it < fetchWindow.start }
|
||||
if (manualFetch || manga.fetchInterval == 0 || isNextUpdateBeforeWindow) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
timeZone,
|
||||
now,
|
||||
fetchWindow,
|
||||
)
|
||||
@@ -179,8 +195,8 @@ class SyncChaptersWithSource(
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = newChapters.size
|
||||
var updatedToAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
val toAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowInstant + (itemCount--).milliseconds)
|
||||
|
||||
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
|
||||
changedOrDuplicateReadUrls.add(chapter.url)
|
||||
@@ -204,27 +220,25 @@ class SyncChaptersWithSource(
|
||||
chapter
|
||||
}
|
||||
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
val toDeleteIds = removedChapters.map { it.id }
|
||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
if (updatedToAdd.isNotEmpty()) {
|
||||
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
||||
}
|
||||
|
||||
if (updatedChapters.isNotEmpty()) {
|
||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
updateManga.awaitUpdateLastUpdate(manga.id)
|
||||
val added = chapterRepository.updateFromRemote(
|
||||
removedIds = removedChapters.map { it.id },
|
||||
added = toAdd,
|
||||
updated = updatedChapters,
|
||||
)
|
||||
|
||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
||||
// A first fetch lists the back catalogue, and chapters coming back under a deleted number were out already
|
||||
val foundRelease = dbChapters.isNotEmpty() &&
|
||||
toAdd.any { chapter ->
|
||||
chapter.scanlator !in excludedScanlators &&
|
||||
!(chapter.isRecognizedNumber && chapter.chapterNumber in deletedChapterNumbers)
|
||||
}
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
updateManga.awaitUpdateFetchIntervalAndLastUpdate(manga, timeZone, now, fetchWindow, foundRelease)
|
||||
|
||||
return added.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import mihon.core.common.extensions.toEpochMillisOrZero
|
||||
import mihon.core.common.extensions.toInstantOrNull
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
|
||||
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Chapter.toSChapter(): SChapter {
|
||||
return SChapter.create().also {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.date_upload = dateUpload.toEpochMillisOrZero()
|
||||
it.chapter_number = chapterNumber.toFloat()
|
||||
it.scanlator = scanlator
|
||||
it.memo = memo
|
||||
@@ -21,25 +20,9 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
|
||||
return this.copy(
|
||||
name = sChapter.name,
|
||||
url = sChapter.url,
|
||||
dateUpload = sChapter.date_upload,
|
||||
dateUpload = sChapter.date_upload.toInstantOrNull(),
|
||||
chapterNumber = sChapter.chapter_number.toDouble(),
|
||||
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
|
||||
memo = sChapter.memo,
|
||||
)
|
||||
}
|
||||
|
||||
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.scanlator = scanlator
|
||||
it.read = read
|
||||
it.bookmark = bookmark
|
||||
it.last_page_read = lastPageRead.toInt()
|
||||
it.date_fetch = dateFetch
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber.toFloat()
|
||||
it.source_order = sourceOrder.toInt()
|
||||
it.memo = memo
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package eu.kanade.domain.download.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
|
||||
@Inject
|
||||
class DeleteDownload(
|
||||
private val sourceManager: SourceManager,
|
||||
private val downloadManager: DownloadManager,
|
||||
) {
|
||||
|
||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) {
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
downloadManager.deleteChapters(chapters.toList(), manga, source)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@Inject
|
||||
class GetExtensionLanguages(
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Inject
|
||||
class GetExtensionSources(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
|
||||
fun subscribe(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
|
||||
val isMultiSource = extension.sources.size > 1
|
||||
val isMultiLangSingleSource =
|
||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.extension.model.Extensions
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
@@ -7,36 +8,33 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@Inject
|
||||
class GetExtensionsByType(
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Extensions> {
|
||||
val showNsfwSources = preferences.showNsfwSource.get()
|
||||
val enabledContentWarnings = preferences.enabledContentWarnings.get()
|
||||
|
||||
return combine(
|
||||
preferences.enabledLanguages.changes(),
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.loadedExtensionsFlow,
|
||||
extensionManager.notLoadedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguages, _installed, _untrusted, _available ->
|
||||
val (updates, installed) = _installed
|
||||
.filter { (showNsfwSources || !it.isNsfw) }
|
||||
.sortedWith(
|
||||
compareBy<Extension.Installed> { !it.isObsolete }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||
val byName = compareBy<Extension, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||
|
||||
val untrusted = _untrusted
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
val (loadedUpdates, loaded) = _loaded.partition { it.hasUpdate }
|
||||
val (notLoadedUpdates, notLoaded) = _notLoaded.partition { it.hasUpdate }
|
||||
|
||||
val updates = (loadedUpdates + notLoadedUpdates).sortedWith(byName)
|
||||
|
||||
val installedSignatures = (_loaded + _notLoaded).groupBy({ it.pkgName }, { it.signatures })
|
||||
val available = _available
|
||||
.filter { extension ->
|
||||
_installed.none { it.pkgName == extension.pkgName } &&
|
||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||
(showNsfwSources || !extension.isNsfw)
|
||||
installedSignatures[extension.pkgName].orEmpty().none { extension.store.signingKey in it } &&
|
||||
extension.contentWarning in enabledContentWarnings
|
||||
}
|
||||
.flatMap { ext ->
|
||||
ext.sources.filter { it.lang in enabledLanguages }
|
||||
@@ -49,9 +47,17 @@ class GetExtensionsByType(
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.sortedWith(
|
||||
compareBy<Extension.Available, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||
.thenBy { it.store.signingKey },
|
||||
)
|
||||
|
||||
Extensions(updates, installed, available, untrusted)
|
||||
Extensions(
|
||||
updates = updates,
|
||||
loaded = loaded.sortedWith(compareBy<Extension.Loaded> { !it.isObsolete }.then(byName)),
|
||||
available = available,
|
||||
notLoaded = notLoaded.sortedWith(byName),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,25 @@ package eu.kanade.domain.extension.interactor
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
@Inject
|
||||
class TrustExtension(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||
val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet()
|
||||
fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>, stores: List<ExtensionStore>): Boolean {
|
||||
val trustedFingerprints = stores.mapTo(HashSet()) { it.signingKey }
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions.get()
|
||||
}
|
||||
@@ -29,4 +37,23 @@ class TrustExtension(
|
||||
fun revokeAll() {
|
||||
preferences.trustedExtensions.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits whenever what counts as trusted changes, either because a store was added or removed or
|
||||
* because an extension was trusted or had its trust revoked. Both sources replay their current
|
||||
* value, which is dropped.
|
||||
*/
|
||||
fun changes(): Flow<Unit> {
|
||||
return merge(
|
||||
// Stores are rewritten whenever their index is refreshed, so only their keys matter here
|
||||
repository.getAllAsFlow()
|
||||
.map { stores -> stores.mapTo(HashSet()) { it.signingKey } }
|
||||
.distinctUntilChanged()
|
||||
.drop(1),
|
||||
preferences.trustedExtensions.changes()
|
||||
.distinctUntilChanged()
|
||||
.drop(1),
|
||||
)
|
||||
.map {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
data class Extensions(
|
||||
val updates: List<Extension.Installed>,
|
||||
val installed: List<Extension.Installed>,
|
||||
val loaded: List<Extension.Loaded>,
|
||||
val available: List<Extension.Available>,
|
||||
val untrusted: List<Extension.Untrusted>,
|
||||
val notLoaded: List<Extension.NotLoaded>,
|
||||
)
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.data.Database
|
||||
import tachiyomi.data.subscribeToList
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@Inject
|
||||
class GetExcludedScanlators(
|
||||
private val database: Database,
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): Set<String> {
|
||||
return database.excluded_scanlatorsQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.awaitAsList()
|
||||
.toSet()
|
||||
return mangaRepository.getExcludedScanlators(mangaId)
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
||||
return database.excluded_scanlatorsQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.subscribeToList()
|
||||
.map { it.toSet() }
|
||||
return mangaRepository.getExcludedScanlatorsAsFlow(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||
import tachiyomi.data.Database
|
||||
import dev.zacsweers.metro.Inject
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@Inject
|
||||
class SetExcludedScanlators(
|
||||
private val database: Database,
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
||||
database.transaction {
|
||||
val currentExcluded = database.excluded_scanlatorsQueries
|
||||
.getExcludedScanlatorsByMangaId(mangaId)
|
||||
.awaitAsList()
|
||||
.toSet()
|
||||
val toAdd = excludedScanlators.minus(currentExcluded)
|
||||
for (scanlator in toAdd) {
|
||||
database.excluded_scanlatorsQueries.insert(mangaId, scanlator)
|
||||
}
|
||||
val toRemove = currentExcluded.minus(excludedScanlators)
|
||||
database.excluded_scanlatorsQueries.remove(mangaId, toRemove)
|
||||
}
|
||||
mangaRepository.setExcludedScanlators(mangaId, excludedScanlators)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@Inject
|
||||
class SetMangaViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
@@ -12,20 +14,18 @@ class SetMangaViewerFlags(
|
||||
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
|
||||
),
|
||||
MangaUpdate(id) {
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetOrientation(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
|
||||
),
|
||||
MangaUpdate(id) {
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Inject
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val fetchInterval: FetchInterval,
|
||||
@@ -22,29 +27,45 @@ class UpdateManga(
|
||||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||
timeZone: TimeZone = TimeZone.currentSystemDefault(),
|
||||
dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone),
|
||||
window: ClosedRange<Instant>? = fetchInterval.getWindow(dateTime.date, timeZone),
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
fetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||
fetchInterval.withFetchInterval(manga, dateTime, timeZone, window),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
|
||||
/**
|
||||
* Updates the fetch interval and marks the chapter list as changed in a single write.
|
||||
*/
|
||||
suspend fun awaitUpdateFetchIntervalAndLastUpdate(
|
||||
manga: Manga,
|
||||
timeZone: TimeZone,
|
||||
dateTime: LocalDateTime,
|
||||
window: ClosedRange<Instant>?,
|
||||
foundRelease: Boolean,
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
fetchInterval.withFetchInterval(manga, dateTime, timeZone, window, foundRelease).apply {
|
||||
lastUpdate = Clock.System.now()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(mangaId) {
|
||||
coverLastModified = Clock.System.now()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||
val dateAdded = when (favorite) {
|
||||
true -> Instant.now().toEpochMilli()
|
||||
false -> 0
|
||||
val update = when (favorite) {
|
||||
true -> MangaUpdate(mangaId) { favoriteAt = Clock.System.now() }
|
||||
false -> MangaUpdate(mangaId) { favoriteAt = null }
|
||||
}
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
|
||||
)
|
||||
return mangaRepository.update(update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import mihon.app.di.appGraph
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
||||
@@ -22,7 +26,7 @@ val Manga.readerOrientation: Long
|
||||
|
||||
val Manga.downloadedFilter: TriState
|
||||
get() {
|
||||
if (Injekt.get<BasePreferences>().downloadedOnly.get()) return TriState.ENABLED_IS
|
||||
if (Injekt.get<Context>().appGraph.basePreferences.downloadedOnly.get()) return TriState.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||
@@ -71,8 +75,8 @@ fun Manga.copyFrom(other: SManga): Manga {
|
||||
)
|
||||
}
|
||||
|
||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get<Context>().appGraph.coverCache): Boolean {
|
||||
return coverCache.hasCustomCover(id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,30 +88,37 @@ fun getComicInfo(
|
||||
urls: List<String>,
|
||||
categories: List<String>?,
|
||||
sourceName: String,
|
||||
) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||
if ((it.rem(1) == 0.0)) {
|
||||
ComicInfo.Number(it.toInt().toString())
|
||||
} else {
|
||||
ComicInfo.Number(it.toString())
|
||||
}
|
||||
},
|
||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||
source = ComicInfo.SourceMihon(sourceName),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
): ComicInfo {
|
||||
val date = chapter.dateUpload?.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
|
||||
return ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||
if ((it.rem(1) == 0.0)) {
|
||||
ComicInfo.Number(it.toInt().toString())
|
||||
} else {
|
||||
ComicInfo.Number(it.toString())
|
||||
}
|
||||
},
|
||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||
source = ComicInfo.SourceMihon(sourceName),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
year = date?.year?.let { ComicInfo.Year(it) },
|
||||
month = date?.month?.number?.let { ComicInfo.Month(it) },
|
||||
day = date?.day?.let { ComicInfo.Day(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -10,6 +11,7 @@ import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.repository.SourceRepository
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
@Inject
|
||||
class GetEnabledSources(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
@@ -7,12 +8,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Inject
|
||||
class GetIncognitoState(
|
||||
private val basePreferences: BasePreferences,
|
||||
private val sourcePreferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
fun await(sourceId: Long?): Boolean {
|
||||
suspend fun await(sourceId: Long?): Boolean {
|
||||
if (basePreferences.incognitoMode.get()) return true
|
||||
if (sourceId == null) return false
|
||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -8,6 +9,7 @@ import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.repository.SourceRepository
|
||||
import java.util.SortedMap
|
||||
|
||||
@Inject
|
||||
class GetLanguagesWithSources(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -9,6 +10,7 @@ import tachiyomi.domain.source.repository.SourceRepository
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.util.Collections
|
||||
|
||||
@Inject
|
||||
class GetSourcesWithFavoriteCount(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
||||
@Inject
|
||||
class SetMigrateSorting(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
@Inject
|
||||
class ToggleIncognito(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
@Inject
|
||||
class ToggleLanguage(
|
||||
val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
@Inject
|
||||
class ToggleSource(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
@Inject
|
||||
class ToggleSourcePin(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package eu.kanade.domain.source.model
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
val Source.icon: ImageBitmap?
|
||||
get() {
|
||||
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
package eu.kanade.domain.source.service
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import mihon.domain.extension.model.ContentWarning
|
||||
import mihon.domain.migration.models.MigrationFlag
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import tachiyomi.core.common.preference.getEnumSet
|
||||
import tachiyomi.core.common.preference.getLongArray
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class SourcePreferences(
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
@@ -36,7 +43,15 @@ class SourcePreferences(
|
||||
-1,
|
||||
)
|
||||
|
||||
val showNsfwSource: Preference<Boolean> = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
val enabledContentWarnings: Preference<Set<ContentWarning>> = preferenceStore.getEnumSet(
|
||||
"enabled_content_warnings",
|
||||
setOf(ContentWarning.SAFE, ContentWarning.MIXED, ContentWarning.NSFW),
|
||||
)
|
||||
|
||||
val applyContentWarningsToInstalled: Preference<Boolean> = preferenceStore.getBoolean(
|
||||
"apply_content_warnings_to_installed",
|
||||
true,
|
||||
)
|
||||
|
||||
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
|
||||
"pref_migration_sorting",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@@ -8,100 +9,96 @@ import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.TimeZone
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Inject
|
||||
class AddTracks(
|
||||
private val insertTrack: InsertTrack,
|
||||
private val upsertTrack: UpsertTrack,
|
||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
private val trackerManager: TrackerManager,
|
||||
private val getHistory: GetHistory,
|
||||
) {
|
||||
|
||||
// TODO: update all trackers based on common data
|
||||
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
val allChapters = getChaptersByMangaId.await(mangaId)
|
||||
val hasReadChapters = allChapters.any { it.read }
|
||||
tracker.bind(item, hasReadChapters)
|
||||
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
val allChapters = getChaptersByMangaId.await(mangaId)
|
||||
val hasReadChapters = allChapters.any { it.read }
|
||||
tracker.bind(item, hasReadChapters)
|
||||
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
var track = item.toDomainTrack(idRequired = false) ?: return@withContext
|
||||
|
||||
insertTrack.await(track)
|
||||
upsertTrack.await(track)
|
||||
|
||||
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||
// Update chapter progress if newer chapters marked read locally
|
||||
if (hasReadChapters) {
|
||||
val latestLocalReadChapterNumber = allChapters
|
||||
.sortedBy { it.chapterNumber }
|
||||
.takeWhile { it.read }
|
||||
.lastOrNull()
|
||||
?.chapterNumber ?: -1.0
|
||||
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||
// Update chapter progress if newer chapters marked read locally
|
||||
if (hasReadChapters) {
|
||||
val latestLocalReadChapterNumber = allChapters
|
||||
.sortedBy { it.chapterNumber }
|
||||
.takeWhile { it.read }
|
||||
.lastOrNull()
|
||||
?.chapterNumber ?: -1.0
|
||||
|
||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||
track = track.copy(
|
||||
lastChapterRead = latestLocalReadChapterNumber,
|
||||
)
|
||||
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
|
||||
.sortedBy { it.readAt }
|
||||
.firstOrNull()
|
||||
?.readAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
)
|
||||
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||
track = track.copy(
|
||||
lastChapterRead = latestLocalReadChapterNumber,
|
||||
)
|
||||
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
}
|
||||
|
||||
syncChapterProgressWithTrack.await(mangaId, track, tracker)
|
||||
if (track.startDate == null) {
|
||||
val firstReadChapterDate = getHistory.await(mangaId)
|
||||
.mapNotNull { it.readAt }
|
||||
.minOrNull()
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.toEpochMilliseconds().convertEpochMillisZone(
|
||||
TimeZone.currentSystemDefault(),
|
||||
TimeZone.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = Instant.fromEpochMilliseconds(startDate),
|
||||
)
|
||||
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncChapterProgressWithTrack.await(mangaId, track, tracker)
|
||||
}
|
||||
|
||||
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
|
||||
withIOContext {
|
||||
trackerManager.loggedInTrackers()
|
||||
.filterIsInstance<EnhancedTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).bind(track)
|
||||
insertTrack.await(track.toDomainTrack(idRequired = false)!!)
|
||||
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withContext(Dispatchers.IO) {
|
||||
trackerManager.loggedInTrackers()
|
||||
.filterIsInstance<EnhancedTracker>()
|
||||
.filter { it.accept(source) }
|
||||
.forEach { service ->
|
||||
try {
|
||||
service.match(manga)?.let { track ->
|
||||
track.manga_id = manga.id
|
||||
(service as Tracker).bind(track)
|
||||
upsertTrack.await(track.toDomainTrack(idRequired = false)!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack(idRequired = false)!!,
|
||||
service,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack(idRequired = false)!!,
|
||||
service,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(
|
||||
LogPriority.WARN,
|
||||
e,
|
||||
) { "Could not match manga: ${manga.title} with service $service" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||
|
||||
@Inject
|
||||
class RefreshTracks(
|
||||
private val getTracks: GetTracks,
|
||||
private val trackerManager: TrackerManager,
|
||||
private val insertTrack: InsertTrack,
|
||||
private val upsertTrack: UpsertTrack,
|
||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||
) {
|
||||
|
||||
@@ -28,10 +31,10 @@ class RefreshTracks(
|
||||
.map { it to trackerManager.get(it.trackerId) }
|
||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||
.map { (track, service) ->
|
||||
async {
|
||||
async(Dispatchers.IO) {
|
||||
return@async try {
|
||||
val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||
insertTrack.await(updatedTrack)
|
||||
upsertTrack.await(updatedTrack)
|
||||
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service)
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
@@ -7,14 +8,15 @@ import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import kotlin.math.max
|
||||
|
||||
@Inject
|
||||
class SyncChapterProgressWithTrack(
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val insertTrack: InsertTrack,
|
||||
private val upsertTrack: UpsertTrack,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
) {
|
||||
|
||||
@@ -33,7 +35,7 @@ class SyncChapterProgressWithTrack(
|
||||
|
||||
val chapterUpdates = sortedChapters
|
||||
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
||||
.map { it.copy(read = true).toChapterUpdate() }
|
||||
.map { ChapterUpdate(it.id) { read = true } }
|
||||
|
||||
// only take into account continuous reading
|
||||
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
||||
@@ -43,7 +45,7 @@ class SyncChapterProgressWithTrack(
|
||||
try {
|
||||
tracker.update(updatedTrack.toDbTrack())
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
insertTrack.await(updatedTrack)
|
||||
upsertTrack.await(updatedTrack)
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.WARN, e)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
||||
import eu.kanade.domain.track.service.DelayedTrackingUpdateWorker
|
||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||
|
||||
@Inject
|
||||
class TrackChapter(
|
||||
private val getTracks: GetTracks,
|
||||
private val trackerManager: TrackerManager,
|
||||
private val insertTrack: InsertTrack,
|
||||
private val upsertTrack: UpsertTrack,
|
||||
private val delayedTrackingStore: DelayedTrackingStore,
|
||||
) {
|
||||
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
||||
withNonCancellableContext {
|
||||
coroutineScope {
|
||||
val tracks = getTracks.await(mangaId)
|
||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
if (tracks.isEmpty()) return@coroutineScope
|
||||
|
||||
tracks.mapNotNull { track ->
|
||||
val service = trackerManager.get(track.trackerId)
|
||||
@@ -39,12 +41,12 @@ class TrackChapter(
|
||||
.toDomainTrack(idRequired = true)!!
|
||||
.copy(lastChapterRead = chapterNumber)
|
||||
service.update(updatedTrack.toDbTrack(), true)
|
||||
insertTrack.await(updatedTrack)
|
||||
upsertTrack.await(updatedTrack)
|
||||
delayedTrackingStore.remove(track.id)
|
||||
} catch (e: Exception) {
|
||||
delayedTrackingStore.add(track.id, chapterNumber)
|
||||
if (setupJobOnFailure) {
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
DelayedTrackingUpdateWorker.setupTask(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package eu.kanade.domain.track.model
|
||||
|
||||
import mihon.core.common.extensions.toEpochMillisOrZero
|
||||
import mihon.core.common.extensions.toInstantOrNull
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
|
||||
|
||||
@@ -25,8 +27,8 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
||||
it.status = status
|
||||
it.score = score
|
||||
it.tracking_url = remoteUrl
|
||||
it.started_reading_date = startDate
|
||||
it.finished_reading_date = finishDate
|
||||
it.started_reading_date = startDate.toEpochMillisOrZero()
|
||||
it.finished_reading_date = finishDate.toEpochMillisOrZero()
|
||||
it.private = private
|
||||
}
|
||||
|
||||
@@ -44,8 +46,8 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
status = status,
|
||||
score = score,
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
startDate = started_reading_date.toInstantOrNull(),
|
||||
finishDate = finished_reading_date.toInstantOrNull(),
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
|
||||
+18
-11
@@ -8,31 +8,38 @@ import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import mihon.app.di.AppGraph
|
||||
import mihon.core.metro.metroGraph
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
class DelayedTrackingUpdateWorker(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val graph: AppGraph = context.metroGraph()
|
||||
|
||||
@Inject lateinit var getTracks: GetTracks
|
||||
|
||||
@Inject lateinit var trackChapter: TrackChapter
|
||||
|
||||
@Inject lateinit var delayedTrackingStore: DelayedTrackingStore
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
graph.inject(this)
|
||||
|
||||
if (runAttemptCount > 3) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val getTracks = Injekt.get<GetTracks>()
|
||||
val trackChapter = Injekt.get<TrackChapter>()
|
||||
|
||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||
|
||||
withIOContext {
|
||||
withContext(Dispatchers.IO) {
|
||||
delayedTrackingStore.getItems()
|
||||
.mapNotNull {
|
||||
val track = getTracks.awaitOne(it.trackId)
|
||||
@@ -60,7 +67,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
|
||||
requiredNetworkType = NetworkType.CONNECTED,
|
||||
)
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
||||
.addTag(TAG)
|
||||
@@ -1,13 +1,19 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.domain.track.model.AutoTrackState
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.mangabaka.MangaBaka
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
@@ -42,6 +48,8 @@ class TrackPreferences(
|
||||
|
||||
val anilistScoreType: Preference<String> = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
val kitsuScoreType: Preference<String> = preferenceStore.getString("kitsu_score_type", Kitsu.RATING_ADVANCED)
|
||||
|
||||
val mangabakaScoreType: Preference<String> = preferenceStore.getString("mangabaka_score_type", MangaBaka.STEP_1)
|
||||
|
||||
val autoUpdateTrack: Preference<Boolean> = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
@@ -2,15 +2,20 @@ package eu.kanade.domain.track.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class DelayedTrackingStore(context: Context) {
|
||||
|
||||
/**
|
||||
* Preference file where queued tracking updates are stored.
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
private val preferences by lazy { context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) }
|
||||
|
||||
fun add(trackId: Long, lastChapterRead: Double) {
|
||||
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package eu.kanade.domain.ui
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
@@ -12,6 +15,8 @@ import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class UiPreferences(
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||
YINYANG(MR.strings.theme_yinyang),
|
||||
YOTSUBA(MR.strings.theme_yotsuba),
|
||||
TOKYONIGHT(MR.strings.theme_tokyonight),
|
||||
MONOCHROME(MR.strings.theme_monochrome),
|
||||
|
||||
// Deprecated
|
||||
|
||||
@@ -3,10 +3,6 @@ package eu.kanade.presentation.browse
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
@@ -23,6 +19,10 @@ import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.automirroredrounded.Help
|
||||
import mihon.icons.materialsymbols.rounded.Public
|
||||
import mihon.icons.materialsymbols.rounded.Refresh
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@@ -88,7 +88,7 @@ fun BrowseSourceContent(
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.local_source_help_guide,
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
icon = MaterialSymbols.AutoMirroredRounded.Help,
|
||||
onClick = onLocalSourceHelpClick,
|
||||
),
|
||||
)
|
||||
@@ -96,17 +96,17 @@ fun BrowseSourceContent(
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.action_retry,
|
||||
icon = Icons.Outlined.Refresh,
|
||||
icon = MaterialSymbols.Rounded.Refresh,
|
||||
onClick = mangaList::refresh,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.action_open_in_web_view,
|
||||
icon = Icons.Outlined.Public,
|
||||
icon = MaterialSymbols.Rounded.Public,
|
||||
onClick = onWebViewClick,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.label_help,
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
icon = MaterialSymbols.AutoMirroredRounded.Help,
|
||||
onClick = onHelpClick,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -14,9 +14,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -35,38 +32,41 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||
import eu.kanade.presentation.browse.components.label
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Eyeglasses2
|
||||
import mihon.icons.materialsymbols.rounded.Settings
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: ExtensionDetailsScreenModel.State,
|
||||
state: ExtensionDetailsViewModel.State.Success,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
@@ -76,15 +76,6 @@ fun ExtensionDetailsScreen(
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||
regex.find(state.extension?.store?.indexUrl.orEmpty())
|
||||
?.let {
|
||||
val (user, repo) = it.destructured
|
||||
"https://github.com/$user/$repo"
|
||||
}
|
||||
?: state.extension?.store?.indexUrl
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
@@ -93,52 +84,34 @@ fun ExtensionDetailsScreen(
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = buildList {
|
||||
if (url != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_open_repo),
|
||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||
onClick = {
|
||||
uriHandler.openUri(url)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_enable_all),
|
||||
onClick = onClickEnableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_disable_all),
|
||||
onClick = onClickDisableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = onClickClearCookies,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
actions = listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_enable_all),
|
||||
onClick = onClickEnableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_disable_all),
|
||||
onClick = onClickDisableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = onClickClearCookies,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
ExtensionDetails(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
onClickStore = state.extension.store
|
||||
?.contact
|
||||
?.website
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { website -> { uriHandler.openUri(website) } },
|
||||
sources = state.sources,
|
||||
incognitoMode = state.isIncognito,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
@@ -152,7 +125,8 @@ fun ExtensionDetailsScreen(
|
||||
@Composable
|
||||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
extension: Extension.Loaded,
|
||||
onClickStore: (() -> Unit)?,
|
||||
sources: List<ExtensionSourceItem>,
|
||||
incognitoMode: Boolean,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
@@ -161,7 +135,8 @@ private fun ExtensionDetails(
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
var showContentWarning by remember { mutableStateOf(false) }
|
||||
val contentWarning = extension.contentWarning.label
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -184,8 +159,9 @@ private fun ExtensionDetails(
|
||||
}
|
||||
Unit
|
||||
}.takeIf { extension.isShared },
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
onClickStore = onClickStore,
|
||||
onClickContentWarning = {
|
||||
showContentWarning = true
|
||||
},
|
||||
onExtIncognitoChange = onClickIncognito,
|
||||
)
|
||||
@@ -203,10 +179,12 @@ private fun ExtensionDetails(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showNsfwWarning) {
|
||||
NsfwWarningDialog(
|
||||
if (showContentWarning && contentWarning != null) {
|
||||
ContentWarningDialog(
|
||||
label = contentWarning.title,
|
||||
description = contentWarning.description,
|
||||
onClickConfirm = {
|
||||
showNsfwWarning = false
|
||||
showContentWarning = false
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -214,14 +192,16 @@ private fun ExtensionDetails(
|
||||
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
extension: Extension.Installed,
|
||||
extIncognitoMode: Boolean,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickStore: (() -> Unit)?,
|
||||
onClickContentWarning: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: (() -> Unit)?,
|
||||
onExtIncognitoChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val contentWarning = extension.contentWarning.label
|
||||
|
||||
Column {
|
||||
Column(
|
||||
@@ -238,11 +218,11 @@ private fun DetailsHeader(
|
||||
"""
|
||||
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
||||
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
||||
NSFW: ${extension.isNsfw}
|
||||
Content warning: ${extension.contentWarning}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (extension is Extension.Installed) {
|
||||
if (extension is Extension.Loaded) {
|
||||
append("\n\n")
|
||||
appendLine(
|
||||
"""
|
||||
@@ -274,19 +254,42 @@ private fun DetailsHeader(
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||
|
||||
Text(
|
||||
text = strippedPkgName,
|
||||
text = extension.pkgName,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
extension.store?.let { store ->
|
||||
Text(
|
||||
text = store.name,
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(enabled = onClickStore != null) { onClickStore?.invoke() }
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.extraSmall,
|
||||
vertical = MaterialTheme.padding.extraSmall / 2,
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
if ((extension as? Extension.Loaded)?.isShared == false) {
|
||||
ExtensionPill(
|
||||
text = stringResource(MR.strings.ext_installer_private),
|
||||
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.extraLarge,
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
@@ -301,23 +304,20 @@ private fun DetailsHeader(
|
||||
InfoDivider()
|
||||
|
||||
InfoText(
|
||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||
secondaryText = stringResource(MR.strings.ext_info_language),
|
||||
)
|
||||
|
||||
if (extension.isNsfw) {
|
||||
if (contentWarning != null) {
|
||||
InfoDivider()
|
||||
|
||||
InfoText(
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = stringResource(MR.strings.ext_nsfw_short),
|
||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
secondaryText = stringResource(MR.strings.ext_info_age_rating),
|
||||
onClick = onClickAgeRating,
|
||||
primaryText = stringResource(contentWarning.title),
|
||||
primaryTextColor = contentWarning.color,
|
||||
secondaryText = stringResource(MR.strings.ext_info_warning),
|
||||
onClick = onClickContentWarning,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -340,10 +340,7 @@ private fun DetailsHeader(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickAppInfo,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.ext_app_info),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
Text(stringResource(MR.strings.ext_app_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +349,7 @@ private fun DetailsHeader(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
||||
title = stringResource(MR.strings.pref_incognito_mode),
|
||||
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
|
||||
icon = MaterialSymbols.Rounded.Eyeglasses2,
|
||||
widget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -375,7 +372,7 @@ private fun InfoText(
|
||||
primaryText: String,
|
||||
secondaryText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
primaryTextColor: Color = Color.Unspecified,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val clickableModifier = if (onClick != null) {
|
||||
@@ -392,13 +389,16 @@ private fun InfoText(
|
||||
Text(
|
||||
text = primaryText,
|
||||
textAlign = TextAlign.Center,
|
||||
style = primaryTextStyle,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = primaryTextColor,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
@@ -407,7 +407,9 @@ private fun InfoText(
|
||||
@Composable
|
||||
private fun InfoDivider() {
|
||||
VerticalDivider(
|
||||
modifier = Modifier.height(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.small)
|
||||
.height(24.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -434,7 +436,7 @@ private fun SourceSwitchPreference(
|
||||
if (source.source is ConfigurableSource) {
|
||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
imageVector = MaterialSymbols.Rounded.Settings,
|
||||
contentDescription = stringResource(MR.strings.label_settings),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
@@ -453,12 +455,20 @@ private fun SourceSwitchPreference(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NsfwWarningDialog(
|
||||
private fun ContentWarningDialog(
|
||||
label: StringResource,
|
||||
description: StringResource,
|
||||
onClickConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(label),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.ext_nsfw_warning))
|
||||
Text(text = stringResource(description))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onClickConfirm) {
|
||||
|
||||
@@ -13,13 +13,7 @@ import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.VerifiedUser
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -27,7 +21,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,7 +30,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@@ -45,17 +40,28 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||
import eu.kanade.presentation.browse.components.label
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Close
|
||||
import mihon.icons.materialsymbols.rounded.Download
|
||||
import mihon.icons.materialsymbols.rounded.Info
|
||||
import mihon.icons.materialsymbols.rounded.Public
|
||||
import mihon.icons.materialsymbols.rounded.Refresh
|
||||
import mihon.icons.materialsymbols.rounded.Settings
|
||||
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
@@ -67,21 +73,20 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun ExtensionScreen(
|
||||
state: ExtensionsScreenModel.State,
|
||||
state: ExtensionsViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
searchQuery: String?,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
@@ -106,7 +111,7 @@ fun ExtensionScreen(
|
||||
actions = listOf(
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.extensionStores,
|
||||
icon = Icons.Outlined.Settings,
|
||||
icon = MaterialSymbols.Rounded.Settings,
|
||||
onClick = { navigator.push(ExtensionStoresScreen()) },
|
||||
),
|
||||
),
|
||||
@@ -133,20 +138,20 @@ fun ExtensionScreen(
|
||||
|
||||
@Composable
|
||||
private fun ExtensionContent(
|
||||
state: ExtensionsScreenModel.State,
|
||||
state: ExtensionsViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
@@ -187,14 +192,14 @@ private fun ExtensionContent(
|
||||
}
|
||||
ExtensionHeader(
|
||||
textRes = header.textRes,
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = header.text,
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -205,21 +210,21 @@ private fun ExtensionContent(
|
||||
contentType = { "item" },
|
||||
key = { item ->
|
||||
when (item.extension) {
|
||||
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
|
||||
is Extension.Installed -> "extension-installed-${item.hashCode()}"
|
||||
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
|
||||
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
|
||||
is Extension.Available -> "extension-available-${item.hashCode()}"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -227,23 +232,29 @@ private fun ExtensionContent(
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onOpenWebView(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.NotLoaded -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -251,20 +262,30 @@ private fun ExtensionContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trustState != null) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onDismissRequest = {
|
||||
trustState = null
|
||||
},
|
||||
)
|
||||
notLoadedState?.let { extension ->
|
||||
val dismiss = { notLoadedState = null }
|
||||
if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
} else {
|
||||
ExtensionNotLoadedDialog(
|
||||
reason = extension.reason,
|
||||
onClickUninstall = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,39 +300,17 @@ private fun ExtensionItem(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
val store = when (extension) {
|
||||
is Extension.Available -> extension.store
|
||||
is Extension.Installed -> extension.store
|
||||
}
|
||||
|
||||
BaseBrowseItem(
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = { onClickItem(extension) },
|
||||
onLongClick = { onLongClickItem(extension) },
|
||||
),
|
||||
modifier = modifier,
|
||||
onClickItem = { onClickItem(extension) },
|
||||
onLongClickItem = { onLongClickItem(extension) },
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val idle = installStep.isCompleted()
|
||||
if (!idle) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
|
||||
val padding by animateDpAsState(
|
||||
targetValue = if (idle) 0.dp else 8.dp,
|
||||
label = "iconPadding",
|
||||
)
|
||||
ExtensionIcon(
|
||||
extension = extension,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
ExtensionItemIcon(extension = extension, installStep = installStep)
|
||||
},
|
||||
action = {
|
||||
ExtensionItemActions(
|
||||
@@ -326,6 +325,7 @@ private fun ExtensionItem(
|
||||
ExtensionItemContent(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
store = store,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
@@ -335,6 +335,7 @@ private fun ExtensionItem(
|
||||
private fun ExtensionItemContent(
|
||||
extension: Extension,
|
||||
installStep: InstallStep,
|
||||
store: ExtensionStore?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -346,63 +347,106 @@ private fun ExtensionItemContent(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||
hasAlreadyShownAnElement = true
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.versionName.isNotEmpty()) {
|
||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||
hasAlreadyShownAnElement = true
|
||||
Text(
|
||||
text = extension.versionName,
|
||||
)
|
||||
}
|
||||
if (store != null) {
|
||||
Text(
|
||||
text = store.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
val warning = when {
|
||||
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
||||
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||
else -> null
|
||||
}
|
||||
if (warning != null) {
|
||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||
hasAlreadyShownAnElement = true
|
||||
Text(
|
||||
text = stringResource(warning).uppercase(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (extension is Extension.Installed && !extension.isShared) {
|
||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = stringResource(MR.strings.ext_installer_private),
|
||||
)
|
||||
}
|
||||
ExtensionItemMetadata(extension = extension, installStep = installStep)
|
||||
}
|
||||
}
|
||||
|
||||
if (!installStep.isCompleted()) {
|
||||
DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
||||
else -> error("Must not show non-install process text")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun ExtensionItemIcon(
|
||||
extension: Extension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.size(48.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val idle = installStep.isCompleted()
|
||||
if (!idle) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(48.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
|
||||
val padding by animateDpAsState(
|
||||
targetValue = if (idle) 0.dp else 8.dp,
|
||||
label = "iconPadding",
|
||||
)
|
||||
ExtensionIcon(
|
||||
extension = extension,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain text for what an extension simply is, pills for the few things worth noticing about it.
|
||||
* Won't look good when it wraps, but it's not like overflowing content can be ellipsized.
|
||||
*/
|
||||
@Composable
|
||||
private fun ExtensionItemMetadata(
|
||||
extension: Extension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall / 2),
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val facts = listOfNotNull(
|
||||
(extension as? Extension.Loaded)
|
||||
?.lang
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { LocaleHelper.getSourceDisplayName(it, LocalContext.current) },
|
||||
extension.versionName.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
if (facts.isNotEmpty()) {
|
||||
Text(
|
||||
text = facts.joinToString(" \u2022 "),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
val notable = listOfNotNull(
|
||||
when (installStep) {
|
||||
InstallStep.Pending -> MR.strings.ext_pending
|
||||
InstallStep.Downloading -> MR.strings.ext_downloading
|
||||
InstallStep.Installing -> MR.strings.ext_installing
|
||||
else -> null
|
||||
}?.let { it to MaterialTheme.colorScheme.primary },
|
||||
when {
|
||||
extension is Extension.NotLoaded ->
|
||||
extension.reason.labelRes?.let { it to MaterialTheme.colorScheme.error }
|
||||
extension is Extension.Loaded && extension.isObsolete ->
|
||||
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
|
||||
else -> null
|
||||
},
|
||||
extension.contentWarning.label?.let { it.title to it.color },
|
||||
(extension as? Extension.Loaded)
|
||||
?.takeIf { !it.isShared }
|
||||
?.let { MR.strings.ext_installer_private to MaterialTheme.colorScheme.onSurfaceVariant },
|
||||
)
|
||||
notable.forEach { (label, color) ->
|
||||
ExtensionPill(text = stringResource(label), color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,76 +460,50 @@ private fun ExtensionItemActions(
|
||||
onClickItemAction: (Extension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when {
|
||||
!isIdle -> {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(MR.strings.action_cancel),
|
||||
)
|
||||
}
|
||||
if (!installStep.isCompleted()) {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Close,
|
||||
contentDescription = stringResource(MR.strings.action_cancel),
|
||||
)
|
||||
}
|
||||
installStep == InstallStep.Error -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(MR.strings.action_retry),
|
||||
)
|
||||
}
|
||||
return@Row
|
||||
}
|
||||
|
||||
val secondaryAction = when (extension) {
|
||||
is Extension.Available -> {
|
||||
(MaterialSymbols.Rounded.Public to MR.strings.action_open_in_web_view)
|
||||
.takeIf { extension.sources.isNotEmpty() }
|
||||
}
|
||||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
is Extension.Loaded -> MaterialSymbols.Rounded.Settings to MR.strings.action_settings
|
||||
is Extension.NotLoaded -> if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||
MaterialSymbols.Rounded.VerifiedUser to MR.strings.ext_trust
|
||||
} else {
|
||||
MaterialSymbols.Rounded.Info to MR.strings.ext_not_loaded_details
|
||||
}
|
||||
}
|
||||
secondaryAction?.let { (icon, label) ->
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(imageVector = icon, contentDescription = stringResource(label))
|
||||
}
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.GetApp,
|
||||
contentDescription = stringResource(MR.strings.ext_update),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.VerifiedUser,
|
||||
contentDescription = stringResource(MR.strings.ext_trust),
|
||||
)
|
||||
}
|
||||
}
|
||||
is Extension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
contentDescription = stringResource(MR.strings.action_open_in_web_view),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.GetApp,
|
||||
contentDescription = stringResource(MR.strings.ext_install),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val action = when {
|
||||
installStep == InstallStep.Error -> MaterialSymbols.Rounded.Refresh to MR.strings.action_retry
|
||||
extension is Extension.Available -> MaterialSymbols.Rounded.Download to MR.strings.ext_install
|
||||
extension is Extension.Installed && extension.hasUpdate -> {
|
||||
MaterialSymbols.Rounded.Download to MR.strings.ext_update
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
action?.let { (icon, label) ->
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(imageVector = icon, contentDescription = stringResource(label))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,6 +543,83 @@ private fun ExtensionHeader(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the reasons the user can act on are worth naming in the row; the rest all mean "broken" to
|
||||
* them and are spelled out in [ExtensionNotLoadedDialog] instead.
|
||||
*/
|
||||
private val Extension.NotLoaded.Reason.labelRes: StringResource?
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.ext_untrusted
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered
|
||||
// The section header already says these aren't loaded; the dialog says why
|
||||
Extension.NotLoaded.Reason.Unsigned,
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion,
|
||||
Extension.NotLoaded.Reason.Malformed,
|
||||
is Extension.NotLoaded.Reason.Failed,
|
||||
-> null
|
||||
}
|
||||
|
||||
private val Extension.NotLoaded.Reason.descriptionRes: StringResource
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.untrusted_extension_message
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered_message
|
||||
Extension.NotLoaded.Reason.Unsigned -> MR.strings.ext_unsigned_message
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion -> MR.strings.ext_unsupported_message
|
||||
Extension.NotLoaded.Reason.Malformed -> MR.strings.ext_malformed_message
|
||||
is Extension.NotLoaded.Reason.Failed -> MR.strings.ext_load_failed_message
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionNotLoadedDialog(
|
||||
reason: Extension.NotLoaded.Reason,
|
||||
onClickUninstall: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.ext_not_loaded_dialog))
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||
Text(text = stringResource(reason.descriptionRes))
|
||||
|
||||
if (reason is Extension.NotLoaded.Reason.Failed) {
|
||||
Text(
|
||||
text = reason.message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
label = context.stringResource(MR.strings.ext_copy_stacktrace),
|
||||
content = reason.stackTrace,
|
||||
)
|
||||
},
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onClickUninstall) {
|
||||
Text(text = stringResource(MR.strings.ext_uninstall))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionTrustDialog(
|
||||
onClickConfirm: () -> Unit,
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@@ -20,7 +20,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
state: SearchScreenModel.State,
|
||||
state: SearchViewModel.State,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
|
||||
@@ -4,14 +4,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchScreen(
|
||||
state: SearchScreenModel.State,
|
||||
state: SearchViewModel.State,
|
||||
fromSourceId: Long?,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
|
||||
@@ -6,12 +6,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.ArrowUpward
|
||||
import androidx.compose.material.icons.outlined.Numbers
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -21,11 +17,17 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceViewModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.ArrowDownward
|
||||
import mihon.icons.materialsymbols.rounded.ArrowUpward
|
||||
import mihon.icons.materialsymbols.rounded.Numbers
|
||||
import mihon.icons.materialsymbols.rounded.SortByAlpha
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
@@ -43,7 +45,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun MigrateSourceScreen(
|
||||
state: MigrateSourceScreenModel.State,
|
||||
state: MigrateSourceViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
@@ -103,11 +105,11 @@ private fun MigrateSourceList(
|
||||
IconButton(onClick = onToggleSortingMode) {
|
||||
when (sortingMode) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
|
||||
Icons.Outlined.SortByAlpha,
|
||||
MaterialSymbols.Rounded.SortByAlpha,
|
||||
contentDescription = stringResource(MR.strings.action_sort_alpha),
|
||||
)
|
||||
SetMigrateSorting.Mode.TOTAL -> Icon(
|
||||
Icons.Outlined.Numbers,
|
||||
MaterialSymbols.Rounded.Numbers,
|
||||
contentDescription = stringResource(MR.strings.action_sort_count),
|
||||
)
|
||||
}
|
||||
@@ -115,11 +117,11 @@ private fun MigrateSourceList(
|
||||
IconButton(onClick = onToggleSortingDirection) {
|
||||
when (sortingDirection) {
|
||||
SetMigrateSorting.Direction.ASCENDING -> Icon(
|
||||
Icons.Outlined.ArrowUpward,
|
||||
MaterialSymbols.Rounded.ArrowUpward,
|
||||
contentDescription = stringResource(MR.strings.action_asc),
|
||||
)
|
||||
SetMigrateSorting.Direction.DESCENDING -> Icon(
|
||||
Icons.Outlined.ArrowDownward,
|
||||
MaterialSymbols.Rounded.ArrowDownward,
|
||||
contentDescription = stringResource(MR.strings.action_desc),
|
||||
)
|
||||
}
|
||||
@@ -156,7 +158,7 @@ private fun MigrateSourceItem(
|
||||
showLanguageInContent = source.lang != "",
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
icon = { SourceIcon(source = source) },
|
||||
icon = { SourceIcon(source = source, modifier = Modifier.size(48.dp)) },
|
||||
action = {
|
||||
BadgeGroup {
|
||||
Badge(text = "$count")
|
||||
|
||||
@@ -10,8 +10,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -23,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@Composable
|
||||
fun SourcesFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
state: SourcesFilterViewModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
@@ -55,7 +54,7 @@ fun SourcesFilterScreen(
|
||||
@Composable
|
||||
private fun SourcesFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
state: SourcesFilterViewModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
@@ -69,7 +68,7 @@ private fun SourcesFilterContent(
|
||||
contentType = "source-filter-header",
|
||||
) {
|
||||
SourcesFilterHeader(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
language = language,
|
||||
enabled = enabled,
|
||||
onClickItem = onClickLanguage,
|
||||
@@ -82,7 +81,7 @@ private fun SourcesFilterContent(
|
||||
contentType = { "source-filter-item" },
|
||||
) { source ->
|
||||
SourcesFilterItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
source = source,
|
||||
enabled = "${source.id}" !in state.disabledSources,
|
||||
onClickItem = onClickSource,
|
||||
|
||||
@@ -6,9 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -21,9 +18,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Keep
|
||||
import mihon.icons.materialsymbols.roundedfilled.Keep
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -40,7 +40,7 @@ import tachiyomi.source.local.isLocal
|
||||
|
||||
@Composable
|
||||
fun SourcesScreen(
|
||||
state: SourcesScreenModel.State,
|
||||
state: SourcesViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source, Listing) -> Unit,
|
||||
onClickPin: (Source) -> Unit,
|
||||
@@ -143,7 +143,7 @@ private fun SourcePinButton(
|
||||
isPinned: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
||||
val icon = if (isPinned) MaterialSymbols.RoundedFilled.Keep else MaterialSymbols.Rounded.Keep
|
||||
val tint = if (isPinned) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
|
||||
@@ -3,12 +3,14 @@ package eu.kanade.presentation.browse.components
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@@ -39,7 +41,7 @@ fun BaseSourceItem(
|
||||
}
|
||||
|
||||
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
||||
SourceIcon(source = source)
|
||||
SourceIcon(source = source, modifier = Modifier.size(48.dp))
|
||||
}
|
||||
|
||||
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.runtime.Composable
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.CollectionsBookmark
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
|
||||
@Composable
|
||||
internal fun InLibraryBadge(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
Badge(
|
||||
imageVector = Icons.Outlined.CollectionsBookmark,
|
||||
imageVector = MaterialSymbols.Rounded.CollectionsBookmark,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,26 @@ package eu.kanade.presentation.browse.components
|
||||
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Dangerous
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.domain.source.model.icon
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledExtensionIcon
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledSourceIcon
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.roundedfilled.Dangerous
|
||||
import mihon.icons.materialsymbols.roundedfilled.Warning
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
@@ -44,24 +34,15 @@ fun SourceIcon(
|
||||
source: Source,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val icon = source.icon
|
||||
|
||||
when {
|
||||
source.isStub && icon == null -> {
|
||||
source.isStub -> {
|
||||
Image(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
imageVector = MaterialSymbols.RoundedFilled.Warning,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
icon != null -> {
|
||||
Image(
|
||||
bitmap = icon,
|
||||
contentDescription = null,
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
source.isLocal() -> {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_local_source),
|
||||
@@ -70,9 +51,12 @@ fun SourceIcon(
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_default_source),
|
||||
val defaultIcon = painterResource(R.mipmap.ic_default_source)
|
||||
AsyncImage(
|
||||
model = InstalledSourceIcon(source.id),
|
||||
contentDescription = null,
|
||||
placeholder = defaultIcon,
|
||||
error = defaultIcon,
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
@@ -96,53 +80,21 @@ fun ExtensionIcon(
|
||||
.clip(MaterialTheme.shapes.extraSmall),
|
||||
)
|
||||
}
|
||||
is Extension.Installed -> {
|
||||
val icon by extension.getIcon(density)
|
||||
when (icon) {
|
||||
Result.Loading -> Box(modifier = modifier)
|
||||
is Result.Success -> Image(
|
||||
bitmap = (icon as Result.Success<ImageBitmap>).value,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
Result.Error -> Image(
|
||||
bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is Extension.Loaded -> {
|
||||
val defaultIcon = painterResource(R.mipmap.ic_default_source)
|
||||
AsyncImage(
|
||||
model = InstalledExtensionIcon(extension.pkgName, extension.versionCode, density),
|
||||
contentDescription = null,
|
||||
placeholder = defaultIcon,
|
||||
error = defaultIcon,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is Extension.Untrusted -> Image(
|
||||
imageVector = Icons.Filled.Dangerous,
|
||||
is Extension.NotLoaded -> Image(
|
||||
imageVector = MaterialSymbols.RoundedFilled.Dangerous,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): State<Result<ImageBitmap>> {
|
||||
val context = LocalContext.current
|
||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||
withIOContext {
|
||||
value = try {
|
||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
|
||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||
Result.Success(
|
||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||
.toBitmap()
|
||||
.asImageBitmap(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result<out T> {
|
||||
data object Loading : Result<Nothing>()
|
||||
data object Error : Result<Nothing>()
|
||||
data class Success<out T>(val value: T) : Result<T>()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.ViewModule
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -18,6 +15,10 @@ import eu.kanade.presentation.components.RadioMenuItem
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.automirroredroundedfilled.ViewList
|
||||
import mihon.icons.materialsymbols.roundedfilled.ViewCompact
|
||||
import mihon.icons.materialsymbols.roundedfilled.ViewModule
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -57,10 +58,12 @@ fun BrowseSourceToolbar(
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||
Icons.AutoMirrored.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
icon = when (displayMode) {
|
||||
LibraryDisplayMode.List -> MaterialSymbols.AutoMirroredRoundedFilled.ViewList
|
||||
LibraryDisplayMode.ComfortableGrid -> MaterialSymbols.RoundedFilled.ViewModule
|
||||
LibraryDisplayMode.CompactGrid,
|
||||
LibraryDisplayMode.CoverOnlyGrid,
|
||||
-> MaterialSymbols.RoundedFilled.ViewCompact
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import mihon.domain.extension.model.ContentWarning
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
internal data class ContentWarningLabel(
|
||||
val title: StringResource,
|
||||
val description: StringResource,
|
||||
val color: Color,
|
||||
)
|
||||
|
||||
internal val ContentWarning.label: ContentWarningLabel?
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = when (this) {
|
||||
ContentWarning.SAFE -> null
|
||||
ContentWarning.MIXED -> ContentWarningLabel(
|
||||
title = MR.strings.ext_content_warning_mixed,
|
||||
description = MR.strings.ext_content_warning_mixed_description,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
ContentWarning.NSFW -> ContentWarningLabel(
|
||||
title = MR.strings.ext_content_warning_nsfw,
|
||||
description = MR.strings.ext_content_warning_nsfw_description,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
internal fun ExtensionPill(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier
|
||||
.background(color = color.copy(alpha = PILL_BACKGROUND_ALPHA), shape = MaterialTheme.shapes.small)
|
||||
.padding(horizontal = MaterialTheme.padding.small, vertical = 2.dp),
|
||||
color = color,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
||||
private const val PILL_BACKGROUND_ALPHA = 0.12f
|
||||
+5
-5
@@ -10,9 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -23,6 +20,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.automirroredrounded.ArrowForward
|
||||
import mihon.icons.materialsymbols.roundedfilled.Error
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -55,7 +55,7 @@ fun GlobalSearchResultItem(
|
||||
Text(text = subtitle)
|
||||
}
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
Icon(imageVector = MaterialSymbols.AutoMirroredRounded.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
content()
|
||||
@@ -90,7 +90,7 @@ fun GlobalSearchErrorResultItem(message: String?) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
|
||||
Icon(imageVector = MaterialSymbols.RoundedFilled.Error, contentDescription = null)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = message ?: stringResource(MR.strings.unknown_error),
|
||||
|
||||
@@ -7,13 +7,10 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -28,6 +25,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.DoneAll
|
||||
import mihon.icons.materialsymbols.rounded.FilterList
|
||||
import mihon.icons.materialsymbols.rounded.Keep
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -72,6 +73,7 @@ fun GlobalSearchToolbar(
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// TODO: make this UX better; it only applies when triggering a new search
|
||||
if (!hideSourceFilter) {
|
||||
@@ -80,7 +82,7 @@ fun GlobalSearchToolbar(
|
||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PushPin,
|
||||
imageVector = MaterialSymbols.Rounded.Keep,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
@@ -95,7 +97,7 @@ fun GlobalSearchToolbar(
|
||||
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DoneAll,
|
||||
imageVector = MaterialSymbols.Rounded.DoneAll,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
@@ -106,7 +108,7 @@ fun GlobalSearchToolbar(
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
VerticalDivider(modifier = Modifier.height(FilterChipDefaults.Height))
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
@@ -114,7 +116,7 @@ fun GlobalSearchToolbar(
|
||||
onClick = { onToggleResults() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
imageVector = MaterialSymbols.Rounded.FilterList,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user