mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-05 22:31:31 +02:00
Compare commits
184 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6294521460 | ||
|
1d6dc1e8b0 | ||
|
da4584e193 | ||
|
935f1fcf3f | ||
|
093e8e5c5a | ||
|
b372657238 | ||
|
dae7d17966 | ||
|
4c6bc8e830 | ||
|
8a3b610775 | ||
|
ac432e2e94 | ||
|
28093935d8 | ||
|
7028b8673a | ||
|
2dc8cf000b | ||
|
f76a3ad15a | ||
|
f33aa1ac92 | ||
|
05012de569 | ||
|
0d19308054 | ||
|
aa6a4953c5 | ||
|
046f09c4bd | ||
|
eddf07f9ac | ||
|
22b5fb58ff | ||
|
4f06c1cc09 | ||
|
7913679f9d | ||
|
0893609ad2 | ||
|
85d168ed5e | ||
|
d3691cc256 | ||
|
7f9406aec9 | ||
|
563bc02113 | ||
|
b702603965 | ||
|
2e2f1ed82d | ||
|
b2765a00d2 | ||
|
0e6d6c087e | ||
|
4f7122d6f0 | ||
|
b763d3e2c2 | ||
|
9957fff2fb | ||
|
debca74e0d | ||
|
1313ff7a16 | ||
|
793d7fbe40 | ||
|
b12ee027ea | ||
|
d7a1ae2734 | ||
|
7566918ee7 | ||
|
7b70b40d30 | ||
|
cd0481592c | ||
|
b93746b01e | ||
|
2b0c28938b | ||
|
4db3817782 | ||
|
ec07843f0c | ||
|
b08b22fdcc | ||
|
919607cd06 | ||
|
d91c7b6093 | ||
|
fab8b17d99 | ||
|
8b28a9bcee | ||
|
79e25451bd | ||
|
0dda64b9d8 | ||
|
181dbbb638 | ||
|
3ce013fa19 | ||
|
fe22f5aa37 | ||
|
1dd81ef1e1 | ||
|
2d0be5b0c9 | ||
|
4d7350e318 | ||
|
49b2b346b6 | ||
|
badc229a23 | ||
|
277d8bad8e | ||
|
8b48d1016b | ||
|
a96fbba3dc | ||
|
d8a530266f | ||
|
e1724d1aa0 | ||
|
1a5b4c2804 | ||
|
2cd52d5a1f | ||
|
ebfbbf0741 | ||
|
eeb683069a | ||
|
7e71a34256 | ||
|
29ee53f461 | ||
|
6a223f34a0 | ||
|
c335ea9103 | ||
|
c97fe71e29 | ||
|
8e81a5e68b | ||
|
b08270d523 | ||
|
34d1e6fa27 | ||
|
a80965f7f1 | ||
|
59ee61039b | ||
|
b7a96e6946 | ||
|
31a3f9e051 | ||
|
42e45e6020 | ||
|
e8c9cb2c2e | ||
|
d592ab2e87 | ||
|
9d6ed93daa | ||
|
34efa8d901 | ||
|
bfc8320aa4 | ||
|
29ec7c125a | ||
|
dce6aacf02 | ||
|
503d0be667 | ||
|
82fd89cee6 | ||
|
643f95f046 | ||
|
9c81f2486c | ||
|
e59d2d381d | ||
|
da90064c94 | ||
|
d53a3828b1 | ||
|
c283abefb0 | ||
|
4bc593861c | ||
|
5a9367603b | ||
|
c01e9f3e92 | ||
|
ae9753a1ea | ||
|
1fe4d6cbd4 | ||
|
2c5f28f277 | ||
|
3a3abc6854 | ||
|
d9a550b935 | ||
|
1617f8eb49 | ||
|
77faab14b1 | ||
|
d60802721b | ||
|
19af85ab61 | ||
|
4a7fe44e0e | ||
|
c5655e8803 | ||
|
d3973f4ad8 | ||
|
bb230fd6a7 | ||
|
e526fd44c6 | ||
|
f61f039a45 | ||
|
79eb02d8f0 | ||
|
814584d35b | ||
|
8751307301 | ||
|
bcff2262b3 | ||
|
04454ecdbe | ||
|
69320e4d09 | ||
|
e86aeee9c4 | ||
|
be37f214d8 | ||
|
1a833e88b1 | ||
|
4c84878adc | ||
|
054198e78f | ||
|
d522d81164 | ||
|
40fe5f8437 | ||
|
3a100c7816 | ||
|
ead9c0cd47 | ||
|
b4ad9ae063 | ||
|
7f2cfb5eb2 | ||
|
cc96f859bc | ||
|
386a714ffe | ||
|
a807722838 | ||
|
3bd8d3ecb7 | ||
|
8ea95cb27f | ||
|
e280fd63b6 | ||
|
addb4ae9ad | ||
|
d6dfd24548 | ||
|
88aff2c77f | ||
|
81effea01c | ||
|
9aef08c333 | ||
|
dcddac5daa | ||
|
e6d96bd348 | ||
|
1909126921 | ||
|
36d5ee0763 | ||
|
5a91d5c611 | ||
|
e332590b1b | ||
|
0106750503 | ||
|
39982c4063 | ||
|
d1a970e3f3 | ||
|
9df21583dc | ||
|
57e6e198b8 | ||
|
3a648e4fa5 | ||
|
6159bc3636 | ||
|
3cfc2be104 | ||
|
9580a00aa6 | ||
|
cb2b0464d0 | ||
|
ef7992f912 | ||
|
261bbef997 | ||
|
a5349a881b | ||
|
2ca2cec02b | ||
|
2f4bb7cadb | ||
|
bb4d9fc81a | ||
|
ee134fce58 | ||
|
79e711efc2 | ||
|
a1c6089791 | ||
|
06efc3b25c | ||
|
f508d10ad1 | ||
|
22d8aad598 | ||
|
76dcf90340 | ||
|
f33a6d2520 | ||
|
41ae8505fe | ||
|
2914d166fe | ||
|
328ec8c752 | ||
|
78f9a84b14 | ||
|
eedece5adf | ||
|
9d6ddb5d91 | ||
|
b8b053b1d7 | ||
|
ed9e13a365 | ||
|
371c1432e2 |
.editorconfig
.github
.gitignore.idea
CHANGELOG.mdREADME.mdapp
build.gradle.ktsgoogle-services.json
src
main
java
eu
kanade
core
domain
presentation
browse
category
components
history
library
manga
more
reader
theme
track
TrackInfoDialogHome.ktTrackInfoDialogHomePreviewProvider.ktTrackerSearch.ktTrackerSearchPreviewProvider.kt
webview
tachiyomi
App.kt
data
backup
models
restore
coil
database
download
export
library
notification
saver
track
BaseTracker.ktTracker.kt
anilist
bangumi
kitsu
model
myanimelist
updater
di
extension
ui
base
delegate
browse
category
history
home
library
main
manga
more
reader
stats
updates
util
test
mihon
core
migration
res
values
buildSrc/src/main/kotlin
core/common
data
domain
build.gradle.kts
gradle.propertiessrc
main
java
mihon
domain
extensionrepo
tachiyomi
domain
category
interactor
download
service
library
service
manga
interactor
release
track
model
test
java
tachiyomi
domain
release
interactor
gradle
androidx.versions.tomlcompose.versions.tomlgradle-daemon-jvm.propertieskotlinx.versions.tomllibs.versions.toml
gradlewwrapper
i18n
build.gradle.kts
src
commonMain
moko-resources
ar
as
base
bg
bn
ca
cs
cv
da
de
el
eo
es
eu
fa
fi
fil
fr
gl
he
hi
hr
hu
in
it
ja
ka-rGE
kk
km
ko
lt
lv
ml
ms
my
nb-rNO
ne
nl
pl
pt-rBR
pt
ro
ru
sa
sc
sk
sq
sr
sv
ta
th
tr
uk
vi
zh-rCN
zh-rTW
presentation-core
settings.gradle.ktssource-api
source-local
telemetry
@@ -1,8 +1,28 @@
|
||||
[*.{kt,kts}]
|
||||
max_line_length = 120
|
||||
indent_size = 4
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
|
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ❌ Help with Extensions
|
||||
url: https://mihon.app/docs/faq/browse/extensions
|
||||
about: For extension-related questions/issues
|
||||
- name: 🖥️ Mihon website
|
||||
url: https://mihon.app/
|
||||
about: Guides, troubleshooting, and answers to common questions
|
||||
|
12
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
12
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -43,9 +43,9 @@ body:
|
||||
attributes:
|
||||
label: Crash logs
|
||||
description: |
|
||||
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||
If you're experiencing crashes, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here.
|
||||
placeholder: |
|
||||
You can paste the crash logs in plain text or upload it as an attachment.
|
||||
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
|
||||
|
||||
- type: input
|
||||
id: mihon-version
|
||||
@@ -53,7 +53,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.17.0"
|
||||
Example: "0.18.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -96,9 +96,9 @@ 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.17.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.18.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -31,7 +31,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.17.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.18.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
6
.github/renovate.json5
vendored
6
.github/renovate.json5
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"extends": ["config:recommended"],
|
||||
"labels": ["Dependencies"],
|
||||
"semanticCommits": "disabled",
|
||||
"packageRules": [
|
||||
@@ -8,6 +8,6 @@
|
||||
"groupName": "GitHub Actions",
|
||||
"matchManagers": ["github-actions"],
|
||||
"pinDigests": true,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
31
.github/workflows/build_pull_request.yml
vendored
31
.github/workflows/build_pull_request.yml
vendored
@@ -19,38 +19,41 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: 'ubuntu-24.04'
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/standardRelease
|
||||
path: app/build/outputs/mapping/release
|
||||
|
58
.github/workflows/build_push.yml
vendored
58
.github/workflows/build_push.yml
vendored
@@ -13,42 +13,41 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: 'ubuntu-24.04'
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Setup Android SDK
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
||||
|
||||
- name: Build app and run unit tests
|
||||
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/standardRelease
|
||||
path: app/build/outputs/mapping/release
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
@@ -60,42 +59,44 @@ jobs:
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
releaseDirectory: app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: '35.0.1'
|
||||
|
||||
- name: Clean up build artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
run: |
|
||||
set -e
|
||||
|
||||
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
|
||||
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
|
||||
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: Mihon ${{ env.VERSION_TAG }}
|
||||
@@ -111,7 +112,7 @@ jobs:
|
||||
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
|
||||
| x86 | ${{ env.APK_X86_SHA }} |
|
||||
| x86_64 | ${{ env.APK_X86_64_SHA }} |
|
||||
|
||||
|
||||
## If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk
|
||||
files: |
|
||||
mihon-${{ env.VERSION_TAG }}.apk
|
||||
@@ -121,5 +122,4 @@ jobs:
|
||||
mihon-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
token: ${{ secrets.MIHON_BOT_TOKEN }}
|
||||
|
19
.github/workflows/lock.yml
vendored
19
.github/workflows/lock.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
# Daily
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
pr-inactive-days: '2'
|
23
.github/workflows/update_website.yml
vendored
Normal file
23
.github/workflows/update_website.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Update website
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- deleted
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
update_website:
|
||||
runs-on: 'ubuntu-24.04'
|
||||
|
||||
steps:
|
||||
- name: Update website
|
||||
run: |
|
||||
curl --fail-with-body -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.MIHON_BOT_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/mihonapp/website/dispatches \
|
||||
-d '{"event_type":"app_release"}'
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,7 @@ build
|
||||
# IDE files
|
||||
*.iml
|
||||
.idea/*
|
||||
!.idea/icon.png
|
||||
!.idea/icon.svg
|
||||
/captures
|
||||
|
||||
# Configuration files
|
||||
|
BIN
.idea/icon.png
generated
BIN
.idea/icon.png
generated
Binary file not shown.
Before ![]() (image error) Size: 62 KiB |
6
.idea/icon.svg
generated
Normal file
6
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 432 432">
|
||||
<circle cx="216" cy="216" r="216" fill="#2e3943"/>
|
||||
<path fill="#f2faff" d="M398.073 216c0 97.433-81.517 176.419-182.073 176.419-100.556 0-182.073-78.986-182.073-176.419S115.444 39.581 216 39.581c100.556 0 182.073 78.986 182.073 176.419Z"/>
|
||||
<path fill="#7ebbed" fill-rule="evenodd" d="M216 359.34c81.702 0 147.934-64.175 147.934-143.34S297.702 72.66 216 72.66 68.065 136.835 68.065 216 134.298 359.34 216 359.34zm0 33.079c100.556 0 182.073-78.986 182.073-176.419S316.556 39.581 216 39.581C115.444 39.581 33.927 118.567 33.927 216S115.444 392.419 216 392.419z" clip-rule="evenodd"/>
|
||||
<path fill="#031019" d="m155.273 168.033-1.227-28.215c3.68.7 8.063.875 18.052.875 12.092 0 28.041-.7 36.279-1.752 3.504-.35 4.907-.876 7.185-2.103l18.928 16.124c-1.753 2.453-2.279 3.505-4.207 8.412-1.576 3.856-8.762 26.113-11.567 35.577 12.97 2.63 20.155 4.557 29.97 8.588 1.226-8.588 1.401-13.144 1.401-28.742 0-4.03-.175-6.31-.7-9.99l30.495 1.051c-.877 4.207-1.052 5.959-1.227 12.794-.701 16.475-1.403 24.361-3.154 36.279 12.092 6.134 12.092 6.134 18.226 9.464 3.154 1.752 3.855 2.102 5.959 2.804l-10.165 32.773c-4.908-4.381-11.743-9.113-21.732-14.721-8.763 20.855-23.31 36.103-45.392 48.195-7.36-9.814-12.97-15.772-21.907-22.783 12.969-6.134 18.928-9.99 25.763-16.475 6.66-6.484 11.04-12.793 15.247-22.258-11.217-5.082-18.403-7.36-30.846-9.989-7.185 21.382-12.969 35.052-18.051 43.29-6.835 11.04-16.124 16.824-26.815 16.824-8.237 0-16.65-3.68-22.784-9.99-7.01-7.185-10.69-17.175-10.69-28.742 0-17.176 8.238-32.072 22.609-41.361 9.288-5.959 19.103-8.588 34.7-9.465 3.155-10.34 5.785-19.278 8.238-29.267-7.712.701-17.35 1.227-29.093 1.752-6.309.175-8.412.35-13.495 1.051zm26.64 53.279c-8.238 1.402-13.145 4.031-17.527 9.64-3.33 3.855-4.907 8.412-4.907 13.32 0 5.432 2.63 9.464 5.959 9.464 4.03 0 8.588-9.114 16.475-32.424z"/>
|
||||
</svg>
|
After (image error) Size: 1.9 KiB |
81
CHANGELOG.md
81
CHANGELOG.md
@@ -12,6 +12,70 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.18.0] - 2025-03-20
|
||||
### Added
|
||||
- Add option to always decode long strip images with SSIV ([@AntsyLich](https://github.com/AntsyLich)) ([`c5655e8`](https://github.com/mihonapp/mihon/commit/c5655e8803bc32d0931657f0b7bc6afeab70feaf))
|
||||
- Change option label ([@AntsyLich](https://github.com/AntsyLich)) ([#1835](https://github.com/mihonapp/mihon/pull/1835))
|
||||
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
|
||||
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
|
||||
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
|
||||
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
|
||||
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
|
||||
- Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161))
|
||||
- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427))
|
||||
- Add option to mark duplicate read chapters as read after library update or while reading ([@AntsyLich](https://github.com/AntsyLich)) ([#1785](https://github.com/mihonapp/mihon/pull/1785), [#1791](https://github.com/mihonapp/mihon/pull/1791), [#1870](https://github.com/mihonapp/mihon/pull/1870))
|
||||
- Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810))
|
||||
- Add `id:` prefix search to library to search by internal DB ID ([@MajorTanya](https://github.com/MajorTanya)) ([#1856](https://github.com/mihonapp/mihon/pull/1856))
|
||||
- Add back option to disable unread chapter badge in library ([@AntsyLich](https://github.com/AntsyLich)) ([#1871](https://github.com/mihonapp/mihon/pull/1871))
|
||||
|
||||
### Changed
|
||||
- Sliders UI ([@AntsyLich](https://github.com/AntsyLich)) ([#1840](https://github.com/mihonapp/mihon/pull/1840))
|
||||
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
||||
- Ignore hidden files/folders for Local Source chapter list ([@BrutuZ](https://github.com/BrutuZ)) ([#1763](https://github.com/mihonapp/mihon/pull/1763))
|
||||
- Migrate to newer Bangumi API ([@MajorTanya](https://github.com/MajorTanya)) ([#1748](https://github.com/mihonapp/mihon/pull/1748))
|
||||
- Now showing manga starting dates in search
|
||||
- Reduced request load by 2-4x in certain situations
|
||||
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#1833](https://github.com/mihonapp/mihon/pull/1833))
|
||||
- Changed the label of chapter swipe settings and renamed the group to "Behavior" ([@AntsyLich](https://github.com/AntsyLich)) ([#1870](https://github.com/mihonapp/mihon/pull/1870))
|
||||
|
||||
### Fixed
|
||||
- Fix MAL `main_picture` nullability breaking search if a result doesn't have a cover set ([@MajorTanya](https://github.com/MajorTanya)) ([#1618](https://github.com/mihonapp/mihon/pull/1618))
|
||||
- Fix Bangumi and MAL tracking 401 errors due to Mihon sending expired credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#1681](https://github.com/mihonapp/mihon/pull/1681), [#1682](https://github.com/mihonapp/mihon/pull/1682))
|
||||
- Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776))
|
||||
- Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734))
|
||||
- Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726))
|
||||
- Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812))
|
||||
- Fix an issue where tracker reading progress is changed to a lower value ([@Animeboynz](https://github.com/Animeboynz)) ([#1795](https://github.com/mihonapp/mihon/pull/1795))
|
||||
- Attempt to fix crash when migrating or removing entries from library ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1828](https://github.com/mihonapp/mihon/pull/1828))
|
||||
|
||||
### Removed
|
||||
- Remove alphabetical category sort option ([@AntsyLich](https://github.com/AntsyLich)) ([#1781](https://github.com/mihonapp/mihon/pull/1781))
|
||||
|
||||
### Other
|
||||
- Add zoned "Current time" to debug info and include year & timezone in logcat output ([@MajorTanya](https://github.com/MajorTanya)) ([#1672](https://github.com/mihonapp/mihon/pull/1672))
|
||||
- Add application package ID to debug info ([@MajorTanya](https://github.com/MajorTanya)) ([#1847](https://github.com/mihonapp/mihon/pull/1847))
|
||||
|
||||
## [v0.17.1] - 2024-12-06
|
||||
### Changed
|
||||
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`76dcf90`](https://github.com/mihonapp/mihon/commit/76dcf903403d565056f44c66d965c1ea8affffc3))
|
||||
|
||||
### Improved
|
||||
- Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396))
|
||||
- Extension repo URLs are now auto-formatted ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([`22d8aad`](https://github.com/mihonapp/mihon/commit/22d8aad598bea8f00f2831779e45a6645392ca0f))
|
||||
|
||||
### Fixed
|
||||
- Fix "currentTab was used multiple times" ([@AntsyLich](https://github.com/AntsyLich)) ([`371c143`](https://github.com/mihonapp/mihon/commit/371c1432e218f6dcf129f05405dceb2cd351c647))
|
||||
- Fix a rare crash when invoking "Mark previous as read" action ([@AntsyLich](https://github.com/AntsyLich)) ([`f508d10`](https://github.com/mihonapp/mihon/commit/f508d10ad13560d7316df8642bc93fe66c05b9a8))
|
||||
- Fix long strip images not loading in some old devices ([@AntsyLich](https://github.com/AntsyLich)) ([`06efc3b`](https://github.com/mihonapp/mihon/commit/06efc3b25c5af51f42448af27a269ee459d9093d))
|
||||
- Switch to hardware bitmap in reader only if device can handle it ([@AntsyLich](https://github.com/AntsyLich)) ([`e6d96bd`](https://github.com/mihonapp/mihon/commit/e6d96bd348ea5d18a005d6465222ad5f5123103e))
|
||||
- Add option to lower the threshold for hardware bitmaps ([@AntsyLich](https://github.com/AntsyLich)) ([`dcddac5`](https://github.com/mihonapp/mihon/commit/dcddac5daaff3ec89c8507c35dc13d345ffdb6d7))
|
||||
- Improve hardware bitmap threshold option ([@AntsyLich](https://github.com/AntsyLich)) ([`d6dfd24`](https://github.com/mihonapp/mihon/commit/d6dfd24548eaa05a8c3e478068fe2e08f2ee4473))
|
||||
- Always use software bitmap on certain devices ([@MajorTanya](https://github.com/MajorTanya)) ([#1543](https://github.com/mihonapp/mihon/pull/1543))
|
||||
- Fix crash after removing last category while it's active in library ([@cuong-tran](https://github.com/cuong-tran)) ([#1450](https://github.com/mihonapp/mihon/pull/1450))
|
||||
- Fix reader transition color scheme in auto background mode ([@cuong-tran](https://github.com/cuong-tran)) ([#1487](https://github.com/mihonapp/mihon/pull/1487))
|
||||
- Fix app update error notification disappearing ([@cuong-tran](https://github.com/cuong-tran)) ([#1476](https://github.com/mihonapp/mihon/pull/1476))
|
||||
- Fix browser not opening in some cases in Honor devices ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([#1520](https://github.com/mihonapp/mihon/pull/1520))
|
||||
|
||||
## [v0.17.0] - 2024-10-26
|
||||
### Added
|
||||
- Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302))
|
||||
@@ -39,7 +103,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Add option to opt out of Analytics and Crashlytics ([@Animeboynz](https://github.com/Animeboynz)) ([#1237](https://github.com/mihonapp/mihon/pull/1237))
|
||||
- Rework Firebase setup ([@AntsyLich](https://github.com/AntsyLich)) ([`15e3f28`](https://github.com/mihonapp/mihon/commit/15e3f28aa36bec3c31f212c572ab57ce960cc862))
|
||||
- Added random library sort ([@jackhamilton](https://github.com/jackhamilton)) ([#1317](https://github.com/mihonapp/mihon/pull/1317))
|
||||
- Make sure random library sort is at the bottom ([@AntsyLich](https://github.com/AntsyLich)) ([`2e2c8d3`](https://github.com/mihonapp/mihon/commit/2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1))
|
||||
- Make sure random library sort is at the bottom ([@AntsyLich](https://github.com/AntsyLich)) ([`2e2c8d3`](https://github.com/mihonapp/mihon/commit/2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1))
|
||||
- Confirmation dialog when removing privately installed extensions ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1320](https://github.com/mihonapp/mihon/pull/1320))
|
||||
- Option to backup non-library read entries ([@Animeboynz](https://github.com/Animeboynz), [@jobobby04](https://github.com/jobobby04), [@AntsyLich](https://github.com/AntsyLich)) ([#1324](https://github.com/mihonapp/mihon/pull/1324))
|
||||
|
||||
@@ -57,7 +121,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Fix crash with `TypeReference` issue when creating extension repo ([@AntsyLich](https://github.com/AntsyLich)) ([#574](https://github.com/mihonapp/mihon/pull/574), [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a))
|
||||
- Fix mishap in [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a) ([@AntsyLich](https://github.com/AntsyLich)) ([`6965e59`](https://github.com/mihonapp/mihon/commit/6965e59a643c67a2bf81b3c69ec70268e5da5797))
|
||||
- Backup and Restore ([@Animeboynz](https://github.com/Animeboynz)) ([#1057](https://github.com/mihonapp/mihon/pull/1057))
|
||||
- Trust extension by repo ([@AntsyLich](https://github.com/AntsyLich)) ([#570](https://github.com/mihonapp/mihon/pull/570))-
|
||||
- Trust extension by repo ([@AntsyLich](https://github.com/AntsyLich)) ([#570](https://github.com/mihonapp/mihon/pull/570))
|
||||
- From M2 ripple to M3 ([@FooIbar](https://github.com/FooIbar)) ([#675](https://github.com/mihonapp/mihon/pull/675))
|
||||
- Increased continue reading button size ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz)) ([`e17f70f`](https://github.com/mihonapp/mihon/commit/e17f70f7226ea031fc1f962c9dfea3e404ba53ad))
|
||||
- Global search "Has result" choice is now sticky ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649))
|
||||
@@ -128,7 +192,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
### Other
|
||||
- Code cleanup
|
||||
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
|
||||
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
|
||||
- Inside `WorkerInfoScreen` ([@AntsyLich](https://github.com/AntsyLich)) ([`5aec8f8`](https://github.com/mihonapp/mihon/commit/5aec8f8018236a38106483da08f9cbc28261ac9b))
|
||||
- Inside `ChapterDownloadIndicator`, `MangaChapterListItem` ([@AntsyLich](https://github.com/AntsyLich)) ([`b7e091d`](https://github.com/mihonapp/mihon/commit/b7e091d5d039e00cababc7daf555280df6cf9c03))
|
||||
- MangaCoverFetcher ([@ivaniskandar](https://github.com/ivaniskandar)) ([`1365695`](https://github.com/mihonapp/mihon/commit/13656959ae0606736f6ca9eb62699dc23e467c2f))
|
||||
@@ -157,7 +221,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Move archive related code to :core:archive ([@AntsyLich](https://github.com/AntsyLich)) ([`bd7b354`](https://github.com/mihonapp/mihon/commit/bd7b35419861df6d426d6ec0a188391910d0f615))
|
||||
- Replace detekt with ktlint via spotless ([@AntsyLich](https://github.com/AntsyLich)) ([#1130](https://github.com/mihonapp/mihon/pull/1130), [#1136](https://github.com/mihonapp/mihon/pull/1136), [#1138](https://github.com/mihonapp/mihon/pull/1138))
|
||||
- Refrain from running spotless on weblate files ([@AntsyLich](https://github.com/AntsyLich)) ([`32d2c2a`](https://github.com/mihonapp/mihon/commit/32d2c2ac1bc224cbda2f09a4023d7d120ea0e954))
|
||||
- Use feature flags in compose compiler plugin ([@AntsyLich](https://github.com/AntsyLich)) ([`8f9a325`](https://github.com/mihonapp/mihon/commit/8f9a325895bb7b94c2ec92dd969094fc30b3b5e2))- PagerPageHolder: lazy init loading indicator ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`a45eb5e`](https://github.com/mihonapp/mihon/commit/a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab))
|
||||
- Use feature flags in compose compiler plugin ([@AntsyLich](https://github.com/AntsyLich)) ([`8f9a325`](https://github.com/mihonapp/mihon/commit/8f9a325895bb7b94c2ec92dd969094fc30b3b5e2))
|
||||
- PagerPageHolder: lazy init loading indicator ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`a45eb5e`](https://github.com/mihonapp/mihon/commit/a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab))
|
||||
- Collect MangaScreen state with lifecycle ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
|
||||
- Add stable marker to Manga data class ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
|
||||
- Use DTOs to parse tracking API responses ([@MajorTanya](https://github.com/MajorTanya)) ([#1103](https://github.com/mihonapp/mihon/pull/1103))
|
||||
@@ -195,7 +260,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Fix extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415))
|
||||
- Fix build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480))
|
||||
- App infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411))
|
||||
- Crash on Pixel devices (was introduced due to compose update) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
|
||||
- Crash on Pixel devices (was introduced due to compose update) ([@AntsyLich](https://github.com/AntsyLich)) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
|
||||
- Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466))
|
||||
- Crash when putting app in background while track date selection dialog is open ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5))
|
||||
- Dates for saved images not following the specification (fixes date issue mainly on Samsung devices) ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
|
||||
@@ -233,7 +298,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
### Fixed
|
||||
- "Flash screen on page change" making the screen full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5))
|
||||
- Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2)
|
||||
- Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2))
|
||||
- Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7))
|
||||
- Inconsistent button height in "Data and storage" for some languages ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202))
|
||||
- Chapter not being marked as read locally when refreshing Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))
|
||||
@@ -258,7 +323,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
||||
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
||||
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.17.0...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.18.0...main
|
||||
[v0.18.0]: https://github.com/mihonapp/mihon/compare/v0.17.1...v0.18.0
|
||||
[v0.17.1]: https://github.com/mihonapp/mihon/compare/v0.17.0...v0.17.1
|
||||
[v0.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0
|
||||
[v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5
|
||||
[v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4
|
||||
|
@@ -68,7 +68,7 @@ The developer(s) of this application does not have any affiliation with the cont
|
||||
|
||||
<pre>
|
||||
Copyright © 2015 Javier Tomás
|
||||
Copyright © 2024 The Mihon Open Source Project
|
||||
Copyright © 2024 Mihon Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import mihon.buildlogic.Config
|
||||
import mihon.buildlogic.getBuildTime
|
||||
import mihon.buildlogic.getCommitCount
|
||||
import mihon.buildlogic.getGitSha
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("mihon.android.application")
|
||||
@@ -11,7 +11,7 @@ plugins {
|
||||
alias(libs.plugins.aboutLibraries)
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
if (Config.includeTelemetry) {
|
||||
pluginManager.apply {
|
||||
apply(libs.plugins.google.services.get().pluginId)
|
||||
apply(libs.plugins.firebase.crashlytics.get().pluginId)
|
||||
@@ -20,69 +20,71 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 8
|
||||
versionName = "0.17.0"
|
||||
versionCode = 11
|
||||
versionName = "0.18.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
buildConfigField("boolean", "PREVIEW", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += supportedAbis
|
||||
}
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
||||
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
||||
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*supportedAbis.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
val debug by getting {
|
||||
applicationIdSuffix = ".dev"
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
val release by getting {
|
||||
isMinifyEnabled = Config.enableCodeShrink
|
||||
isShrinkResources = Config.enableCodeShrink
|
||||
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
|
||||
}
|
||||
|
||||
val commonMatchingFallbacks = listOf(release.name)
|
||||
|
||||
create("foss") {
|
||||
initWith(release)
|
||||
|
||||
applicationIdSuffix = ".foss"
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
create("preview") {
|
||||
initWith(getByName("release"))
|
||||
buildConfigField("boolean", "PREVIEW", "true")
|
||||
initWith(release)
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
val debugType = getByName("debug")
|
||||
versionNameSuffix = debugType.versionNameSuffix
|
||||
applicationIdSuffix = debugType.applicationIdSuffix
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
versionNameSuffix = debug.versionNameSuffix
|
||||
signingConfig = debug.signingConfig
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
initWith(release)
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
isDebuggable = false
|
||||
isProfileable = true
|
||||
versionNameSuffix = "-benchmark"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
|
||||
signingConfig = debug.signingConfig
|
||||
|
||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,39 +93,46 @@ android {
|
||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions.add("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
||||
dimension = "default"
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
jniLibs {
|
||||
keepDebugSymbols += listOf(
|
||||
"libandroidx.graphics.path",
|
||||
"libarchive-jni",
|
||||
"libconscrypt_jni",
|
||||
"libimagedecoder",
|
||||
"libquickjs",
|
||||
"libsqlite3x",
|
||||
)
|
||||
.map { "**/$it.so" }
|
||||
}
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"kotlin-tooling-metadata.json",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/**/*.properties",
|
||||
"META-INF/**/LICENSE.txt",
|
||||
"META-INF/*.properties",
|
||||
"META-INF/**/*.properties",
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.version",
|
||||
),
|
||||
)
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/README.md",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInApk = Config.includeDependencyInfo
|
||||
includeInBundle = Config.includeDependencyInfo
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -142,6 +151,24 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.i18n)
|
||||
implementation(projects.core.archive)
|
||||
@@ -153,6 +180,7 @@ dependencies {
|
||||
implementation(projects.domain)
|
||||
implementation(projects.presentationCore)
|
||||
implementation(projects.presentationWidget)
|
||||
implementation(projects.telemetry)
|
||||
|
||||
// Compose
|
||||
implementation(compose.activity)
|
||||
@@ -241,15 +269,11 @@ dependencies {
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
"standardImplementation"(platform(libs.firebase.bom))
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
"standardImplementation"(libs.firebase.crashlytics)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
@@ -279,28 +303,6 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-Xcontext-receivers",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(kotlinx.gradle)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
@@ -45,21 +46,6 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing all elements not matching the given [predicate].
|
||||
*
|
||||
@@ -70,27 +56,7 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only the non-null results of applying the
|
||||
* given [transform] function to each element in the original collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||
contract { callsInPlace(transform) }
|
||||
val destination = ArrayList<R>()
|
||||
fastForEach { element ->
|
||||
transform(element)?.let(destination::add)
|
||||
}
|
||||
return destination
|
||||
return fastFilter { !predicate(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,26 +97,3 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
||||
fastForEach { if (predicate(it)) --count }
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements from the given collection
|
||||
* having distinct keys returned by the given [selector] function.
|
||||
*
|
||||
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
||||
* The elements in the resulting list are in the same order as they were in the source collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
||||
contract { callsInPlace(selector) }
|
||||
val set = HashSet<K>()
|
||||
val list = ArrayList<T>()
|
||||
fastForEach {
|
||||
val key = selector(it)
|
||||
if (set.add(key)) list.add(it)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
@@ -13,9 +13,11 @@ 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
|
||||
@@ -109,7 +111,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { RenameCategory(get()) }
|
||||
addFactory { ReorderCategory(get()) }
|
||||
addFactory { UpdateCategory(get()) }
|
||||
addFactory { DeleteCategory(get()) }
|
||||
addFactory { DeleteCategory(get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
addFactory { GetDuplicateLibraryManga(get()) }
|
||||
@@ -151,7 +153,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
addFactory { FilterChaptersForDownload(get(), get(), get()) }
|
||||
|
||||
@@ -191,5 +193,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
addFactory { ToggleIncognito(get()) }
|
||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -30,4 +31,8 @@ class BasePreferences(
|
||||
}
|
||||
|
||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
||||
|
||||
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
||||
|
||||
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ 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
|
||||
@@ -34,6 +35,7 @@ class SyncChaptersWithSource(
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
private val getExcludedScanlators: GetExcludedScanlators,
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -145,12 +147,18 @@ class SyncChaptersWithSource(
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Chapter>()
|
||||
val changedOrDuplicateReadUrls = mutableSetOf<String>()
|
||||
|
||||
val deletedChapterNumbers = TreeSet<Double>()
|
||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||
|
||||
val readChapterNumbers = dbChapters
|
||||
.asSequence()
|
||||
.filter { it.read && it.isRecognizedNumber }
|
||||
.map { it.chapterNumber }
|
||||
.toSet()
|
||||
|
||||
removedChapters.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
@@ -160,12 +168,20 @@ class SyncChaptersWithSource(
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
|
||||
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// 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--)
|
||||
|
||||
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
|
||||
changedOrDuplicateReadUrls.add(chapter.url)
|
||||
chapter = chapter.copy(read = true)
|
||||
}
|
||||
|
||||
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
chapter = chapter.copy(
|
||||
@@ -178,7 +194,7 @@ class SyncChaptersWithSource(
|
||||
chapter = chapter.copy(dateFetch = it)
|
||||
}
|
||||
|
||||
reAdded.add(chapter)
|
||||
changedOrDuplicateReadUrls.add(chapter.url)
|
||||
|
||||
chapter
|
||||
}
|
||||
@@ -202,12 +218,8 @@ class SyncChaptersWithSource(
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
updateManga.awaitUpdateLastUpdate(manga.id)
|
||||
|
||||
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||
|
||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot {
|
||||
it.url in reAddedUrls || it.scanlator in excludedScanlators
|
||||
}
|
||||
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
|
||||
|
||||
val Manga.downloadedFilter: TriState
|
||||
get() {
|
||||
if (forceDownloaded()) return TriState.ENABLED_IS
|
||||
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||
@@ -34,9 +34,6 @@ fun Manga.chaptersFiltered(): Boolean {
|
||||
downloadedFilter != TriState.DISABLED ||
|
||||
bookmarkedFilter != TriState.DISABLED
|
||||
}
|
||||
fun Manga.forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
fun Manga.toSManga(): SManga = SManga.create().also {
|
||||
it.url = url
|
||||
|
@@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class GetIncognitoState(
|
||||
private val basePreferences: BasePreferences,
|
||||
private val sourcePreferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
fun await(sourceId: Long?): Boolean {
|
||||
if (basePreferences.incognitoMode().get()) return true
|
||||
if (sourceId == null) return false
|
||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
||||
|
||||
return extensionPackage in sourcePreferences.incognitoExtensions().get()
|
||||
}
|
||||
|
||||
fun subscribe(sourceId: Long?): Flow<Boolean> {
|
||||
if (sourceId == null) return basePreferences.incognitoMode().changes()
|
||||
|
||||
return combine(
|
||||
basePreferences.incognitoMode().changes(),
|
||||
sourcePreferences.incognitoExtensions().changes(),
|
||||
extensionManager.getExtensionPackageAsFlow(sourceId),
|
||||
) { incognito, incognitoExtensions, extensionPackage ->
|
||||
incognito || (extensionPackage in incognitoExtensions)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
class ToggleIncognito(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
fun await(extensions: String, enable: Boolean) {
|
||||
preferences.incognitoExtensions().getAndSet {
|
||||
if (enable) it.plus(extensions) else it.minus(extensions)
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,6 +22,8 @@ class SourcePreferences(
|
||||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong(
|
||||
|
@@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
|
||||
status = other.status,
|
||||
startDate = other.startDate,
|
||||
finishDate = other.finishDate,
|
||||
private = other.private,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
||||
it.tracking_url = remoteUrl
|
||||
it.started_reading_date = startDate
|
||||
it.finished_reading_date = finishDate
|
||||
it.private = private
|
||||
}
|
||||
|
||||
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
@@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class AppTheme(val titleRes: StringResource?) {
|
||||
@@ -13,13 +12,14 @@ enum class AppTheme(val titleRes: StringResource?) {
|
||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||
|
||||
// TODO: re-enable for preview
|
||||
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
||||
NORD(MR.strings.theme_nord.takeUnless { isReleaseBuildType }),
|
||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||
TAKO(MR.strings.theme_tako),
|
||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||
YINYANG(MR.strings.theme_yinyang),
|
||||
YOTSUBA(MR.strings.theme_yotsuba),
|
||||
MONOCHROME(MR.strings.theme_monochrome),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
|
@@ -35,8 +35,10 @@ 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.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
|
||||
@@ -48,6 +50,7 @@ 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
|
||||
@@ -72,6 +75,7 @@ fun ExtensionDetailsScreen(
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
@@ -140,9 +144,11 @@ fun ExtensionDetailsScreen(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
sources = state.sources,
|
||||
incognitoMode = state.isIncognito,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickSource = onClickSource,
|
||||
onClickIncognito = onClickIncognito,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -152,9 +158,11 @@ private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
sources: ImmutableList<ExtensionSourceItem>,
|
||||
incognitoMode: Boolean,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
onClickIncognito: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
@@ -171,6 +179,7 @@ private fun ExtensionDetails(
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
extIncognitoMode = incognitoMode,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
@@ -182,6 +191,7 @@ private fun ExtensionDetails(
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
onExtIncognitoChange = onClickIncognito,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,9 +219,11 @@ private fun ExtensionDetails(
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
extIncognitoMode: Boolean,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: (() -> Unit)?,
|
||||
onExtIncognitoChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -219,9 +231,8 @@ private fun DetailsHeader(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
bottom = MaterialTheme.padding.small,
|
||||
)
|
||||
@@ -313,12 +324,9 @@ private fun DetailsHeader(
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.padding(top = MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
@@ -341,6 +349,24 @@ private fun DetailsHeader(
|
||||
}
|
||||
}
|
||||
|
||||
TextPreferenceWidget(
|
||||
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),
|
||||
widget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Switch(
|
||||
checked = extIncognitoMode,
|
||||
onCheckedChange = onExtIncognitoChange,
|
||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
@@ -2,22 +2,24 @@ package eu.kanade.presentation.category
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.category.components.CategoryListItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -31,11 +33,9 @@ import tachiyomi.presentation.core.util.plus
|
||||
fun CategoryScreen(
|
||||
state: CategoryScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickSortAlphabetically: () -> Unit,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onClickMoveUp: (Category) -> Unit,
|
||||
onClickMoveDown: (Category) -> Unit,
|
||||
onChangeOrder: (Category, Int) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -44,17 +44,6 @@ fun CategoryScreen(
|
||||
AppBar(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_sort),
|
||||
icon = Icons.Outlined.SortByAlpha,
|
||||
onClick = onClickSortAlphabetically,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
@@ -76,13 +65,10 @@ fun CategoryScreen(
|
||||
CategoryContent(
|
||||
categories = state.categories,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues +
|
||||
topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
paddingValues = paddingValues,
|
||||
onClickRename = onClickRename,
|
||||
onClickDelete = onClickDelete,
|
||||
onMoveUp = onClickMoveUp,
|
||||
onMoveDown = onClickMoveDown,
|
||||
onChangeOrder = onChangeOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -94,28 +80,44 @@ private fun CategoryContent(
|
||||
paddingValues: PaddingValues,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onChangeOrder: (Category, Int) -> Unit,
|
||||
) {
|
||||
val categoriesState = remember { categories.toMutableStateList() }
|
||||
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
|
||||
val item = categoriesState.removeAt(from.index)
|
||||
categoriesState.add(to.index, item)
|
||||
onChangeOrder(item, to.index)
|
||||
}
|
||||
|
||||
LaunchedEffect(categories) {
|
||||
if (!reorderableState.isAnyItemDragging) {
|
||||
categoriesState.clear()
|
||||
categoriesState.addAll(categories)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
contentPadding = paddingValues +
|
||||
topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = categories,
|
||||
key = { _, category -> "category-${category.id}" },
|
||||
) { index, category ->
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
canMoveUp = index != 0,
|
||||
canMoveDown = index != categories.lastIndex,
|
||||
onMoveUp = onMoveUp,
|
||||
onMoveDown = onMoveDown,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
)
|
||||
items(
|
||||
items = categoriesState,
|
||||
key = { category -> category.key },
|
||||
) { category ->
|
||||
ReorderableItem(reorderableState, category.key) {
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Category.key inline get() = "category-$id"
|
||||
|
@@ -193,35 +193,6 @@ fun CategoryDeleteDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSort: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onSort()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_sort_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.sort_category_confirmation))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeCategoryDialog(
|
||||
initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
|
@@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DragHandle
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -19,57 +16,42 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun CategoryListItem(
|
||||
fun ReorderableCollectionItemScope.CategoryListItem(
|
||||
category: Category,
|
||||
canMoveUp: Boolean,
|
||||
canMoveDown: Boolean,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onRename() }
|
||||
.clickable(onClick = onRename)
|
||||
.padding(vertical = MaterialTheme.padding.small)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
start = MaterialTheme.padding.small,
|
||||
end = MaterialTheme.padding.medium,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DragHandle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(MaterialTheme.padding.medium)
|
||||
.draggableHandle(),
|
||||
)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
@@ -77,7 +59,10 @@ fun CategoryListItem(
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = stringResource(MR.strings.action_delete),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
@@ -14,7 +15,6 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -33,20 +33,13 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
fun TabbedScreen(
|
||||
titleRes: StringResource,
|
||||
tabs: ImmutableList<TabContent>,
|
||||
startIndex: Int? = null,
|
||||
state: PagerState = rememberPagerState { tabs.size },
|
||||
searchQuery: String? = null,
|
||||
onChangeSearchQuery: (String?) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberPagerState { tabs.size }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(startIndex) {
|
||||
if (startIndex != null) {
|
||||
state.scrollToPage(startIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val tab = tabs[state.currentPage]
|
||||
|
@@ -38,6 +38,7 @@ fun HistoryScreen(
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
onClickCover: (mangaId: Long) -> Unit,
|
||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||
onClickFavorite: (mangaId: Long) -> Unit,
|
||||
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -84,6 +85,7 @@ fun HistoryScreen(
|
||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
||||
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,6 +99,7 @@ private fun HistoryScreenContent(
|
||||
onClickCover: (HistoryWithRelations) -> Unit,
|
||||
onClickResume: (HistoryWithRelations) -> Unit,
|
||||
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||
onClickFavorite: (HistoryWithRelations) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -126,6 +129,7 @@ private fun HistoryScreenContent(
|
||||
onClickCover = { onClickCover(value) },
|
||||
onClickResume = { onClickResume(value) },
|
||||
onClickDelete = { onClickDelete(value) },
|
||||
onClickFavorite = { onClickFavorite(value) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -152,6 +156,7 @@ internal fun HistoryScreenPreviews(
|
||||
onClickCover = {},
|
||||
onClickResume = { _, _ -> run {} },
|
||||
onDialogChange = {},
|
||||
onClickFavorite = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -39,6 +40,7 @@ fun HistoryItem(
|
||||
onClickCover: () -> Unit,
|
||||
onClickResume: () -> Unit,
|
||||
onClickDelete: () -> Unit,
|
||||
onClickFavorite: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -82,6 +84,16 @@ fun HistoryItem(
|
||||
)
|
||||
}
|
||||
|
||||
if (!history.coverData.isMangaFavorite) {
|
||||
IconButton(onClick = onClickFavorite) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FavoriteBorder,
|
||||
contentDescription = stringResource(MR.strings.add_to_library),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onClickDelete) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
@@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
|
||||
onClickCover = {},
|
||||
onClickResume = {},
|
||||
onClickDelete = {},
|
||||
onClickFavorite = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -19,8 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
@@ -117,10 +117,7 @@ private fun ColumnScope.FilterPage(
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if (
|
||||
(isDevFlavor || isPreviewBuildType) &&
|
||||
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
|
||||
) {
|
||||
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
@@ -255,15 +252,16 @@ private fun ColumnScope.DisplayPage(
|
||||
|
||||
val columns by columnPreference.collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_library_columns),
|
||||
max = 10,
|
||||
value = columns,
|
||||
valueRange = 0..10,
|
||||
label = stringResource(MR.strings.pref_library_columns),
|
||||
valueText = if (columns > 0) {
|
||||
stringResource(MR.strings.pref_library_columns_per_row, columns)
|
||||
columns.toString()
|
||||
} else {
|
||||
stringResource(MR.strings.label_default)
|
||||
stringResource(MR.strings.label_auto)
|
||||
},
|
||||
onChange = columnPreference::set,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -272,6 +270,10 @@ private fun ColumnScope.DisplayPage(
|
||||
label = stringResource(MR.strings.action_display_download_badge),
|
||||
pref = screenModel.libraryPreferences.downloadBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_unread_badge),
|
||||
pref = screenModel.libraryPreferences.unreadBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_local_badge),
|
||||
pref = screenModel.libraryPreferences.localBadge(),
|
||||
|
@@ -21,11 +21,12 @@ internal fun LibraryTabs(
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||
Column(
|
||||
modifier = Modifier.zIndex(1f),
|
||||
) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
selectedTabIndex = currentPageIndex,
|
||||
edgePadding = 0.dp,
|
||||
// TODO: use default when width is fixed upstream
|
||||
// https://issuetracker.google.com/issues/242879624
|
||||
@@ -33,7 +34,7 @@ internal fun LibraryTabs(
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
selected = currentPageIndex == index,
|
||||
onClick = { onTabItemClick(index) },
|
||||
text = {
|
||||
TabText(
|
||||
|
@@ -21,13 +21,14 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.manga.model.downloadedFilter
|
||||
import eu.kanade.domain.manga.model.forceDownloaded
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -40,6 +41,8 @@ import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun ChapterSettingsDialog(
|
||||
@@ -63,6 +66,8 @@ fun ChapterSettingsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = persistentListOf(
|
||||
@@ -97,7 +102,7 @@ fun ChapterSettingsDialog(
|
||||
FilterPage(
|
||||
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
|
||||
onDownloadFilterChanged = onDownloadFilterChanged
|
||||
.takeUnless { manga?.forceDownloaded() == true },
|
||||
.takeUnless { downloadedOnly },
|
||||
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
|
||||
onUnreadFilterChanged = onUnreadFilterChanged,
|
||||
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
|
||||
|
@@ -87,7 +87,7 @@ fun MangaScreen(
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -141,7 +141,7 @@ fun MangaScreen(
|
||||
nextUpdate = nextUpdate,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
navigateUp = navigateUp,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
@@ -176,7 +176,7 @@ fun MangaScreen(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
nextUpdate = nextUpdate,
|
||||
onBackClicked = onBackClicked,
|
||||
navigateUp = navigateUp,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
@@ -214,7 +214,7 @@ private fun MangaScreenSmallImpl(
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -265,14 +265,13 @@ private fun MangaScreenSmallImpl(
|
||||
)
|
||||
}
|
||||
|
||||
val internalOnBackPressed = {
|
||||
BackHandler(onBack = {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
})
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -285,20 +284,18 @@ private fun MangaScreenSmallImpl(
|
||||
val isFirstItemScrolled by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
val titleAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible) 1f else 0f,
|
||||
label = "Top Bar Title",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
val backgroundAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||
label = "Top Bar Background",
|
||||
)
|
||||
MangaToolbar(
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { animatedTitleAlpha },
|
||||
backgroundAlphaProvider = { animatedBgAlpha },
|
||||
hasFilters = state.filterActive,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
navigateUp = navigateUp,
|
||||
onClickFilter = onFilterClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
@@ -306,8 +303,11 @@ private fun MangaScreenSmallImpl(
|
||||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onCancelActionMode = { onAllChapterSelected(false) },
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
titleAlphaProvider = { titleAlpha },
|
||||
backgroundAlphaProvider = { backgroundAlpha },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -458,7 +458,7 @@ fun MangaScreenLargeImpl(
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
@@ -515,14 +515,13 @@ fun MangaScreenLargeImpl(
|
||||
|
||||
val chapterListState = rememberLazyListState()
|
||||
|
||||
val internalOnBackPressed = {
|
||||
BackHandler(onBack = {
|
||||
if (isAnySelected) {
|
||||
onAllChapterSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
})
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -532,19 +531,20 @@ fun MangaScreenLargeImpl(
|
||||
MangaToolbar(
|
||||
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
|
||||
title = state.manga.title,
|
||||
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
hasFilters = state.filterActive,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
navigateUp = navigateUp,
|
||||
onClickFilter = onFilterButtonClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
onClickEditCategory = onEditCategoryClicked,
|
||||
onClickRefresh = onRefresh,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
onCancelActionMode = { onAllChapterSelected(false) },
|
||||
actionModeCounter = selectedChapterCount,
|
||||
onSelectAll = { onAllChapterSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
titleAlphaProvider = { 1f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
|
@@ -19,8 +19,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -109,7 +108,7 @@ fun SetIntervalDialog(
|
||||
}
|
||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||
|
||||
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
|
||||
if (onValueChanged != null && (!isReleaseBuildType)) {
|
||||
Text(stringResource(MR.strings.manga_interval_custom_amount))
|
||||
|
||||
BoxWithConstraints(
|
||||
|
@@ -1,18 +1,12 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -20,12 +14,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||
import eu.kanade.presentation.components.UpIcon
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -35,9 +29,8 @@ import tachiyomi.presentation.core.theme.active
|
||||
@Composable
|
||||
fun MangaToolbar(
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
hasFilters: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
onClickShare: (() -> Unit)?,
|
||||
onClickDownload: ((DownloadAction) -> Unit)?,
|
||||
@@ -47,118 +40,111 @@ fun MangaToolbar(
|
||||
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onCancelActionMode: () -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
) {
|
||||
Column(
|
||||
val isActionMode = actionModeCounter > 0
|
||||
AppBar(
|
||||
titleContent = {
|
||||
if (isActionMode) {
|
||||
AppBarTitle(actionModeCounter.toString())
|
||||
} else {
|
||||
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider()))
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
|
||||
backgroundColor = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
var downloadExpanded by remember { mutableStateOf(false) }
|
||||
if (onClickDownload != null) {
|
||||
val onDismissRequest = { downloadExpanded = false }
|
||||
DownloadDropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onClickDownload,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
}
|
||||
|
||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
||||
if (isActionMode) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onInvertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
var downloadExpanded by remember { mutableStateOf(false) }
|
||||
)
|
||||
return@apply
|
||||
}
|
||||
if (onClickDownload != null) {
|
||||
val onDismissRequest = { downloadExpanded = false }
|
||||
DownloadDropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDownloadClicked = onClickDownload,
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.manga_download),
|
||||
icon = Icons.Outlined.Download,
|
||||
onClick = { downloadExpanded = !downloadExpanded },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
if (onClickDownload != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.manga_download),
|
||||
icon = Icons.Outlined.Download,
|
||||
onClick = { downloadExpanded = !downloadExpanded },
|
||||
),
|
||||
)
|
||||
}
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = filterTint,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_webview_refresh),
|
||||
onClick = onClickRefresh,
|
||||
),
|
||||
)
|
||||
if (onClickEditCategory != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
onClick = onClickEditCategory,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickMigrate != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_migrate),
|
||||
onClick = onClickMigrate,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickShare != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = onClickShare,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = filterTint,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_webview_refresh),
|
||||
onClick = onClickRefresh,
|
||||
),
|
||||
)
|
||||
if (onClickEditCategory != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
onClick = onClickEditCategory,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickMigrate != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_migrate),
|
||||
onClick = onClickMigrate,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onClickShare != null) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_share),
|
||||
onClick = onClickShare,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
|
||||
),
|
||||
)
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
isActionMode = isActionMode,
|
||||
onCancelActionMode = onCancelActionMode,
|
||||
)
|
||||
}
|
||||
|
@@ -1,12 +1,6 @@
|
||||
package eu.kanade.presentation.more
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
@@ -40,7 +34,6 @@ fun MoreScreen(
|
||||
onDownloadedOnlyChange: (Boolean) -> Unit,
|
||||
incognitoMode: Boolean,
|
||||
onIncognitoModeChange: (Boolean) -> Unit,
|
||||
isFDroid: Boolean,
|
||||
onClickDownloadQueue: () -> Unit,
|
||||
onClickCategories: () -> Unit,
|
||||
onClickStats: () -> Unit,
|
||||
@@ -50,19 +43,7 @@ fun MoreScreen(
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
),
|
||||
) {
|
||||
if (isFDroid) {
|
||||
// Don't really care about slow updaters now
|
||||
}
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
Scaffold { contentPadding ->
|
||||
ScrollbarLazyColumn(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
) {
|
||||
|
@@ -4,7 +4,6 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
@@ -32,12 +31,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import eu.kanade.tachiyomi.util.system.telemetryIncluded
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
@@ -111,12 +112,14 @@ internal class PermissionStep : OnboardingStep {
|
||||
onButtonClick = {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
data = "package:${context.packageName}".toUri()
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
|
||||
if (!telemetryIncluded) return@Column
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -17,7 +18,7 @@ sealed class Preference {
|
||||
sealed class PreferenceItem<T> : Preference() {
|
||||
abstract val subtitle: String?
|
||||
abstract val icon: ImageVector?
|
||||
abstract val onValueChanged: suspend (newValue: T) -> Boolean
|
||||
abstract val onValueChanged: suspend (value: T) -> Boolean
|
||||
|
||||
/**
|
||||
* A basic [PreferenceItem] that only displays texts.
|
||||
@@ -25,57 +26,58 @@ sealed class Preference {
|
||||
data class TextPreference(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : PreferenceItem<String>()
|
||||
) : PreferenceItem<String>() {
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that provides a two-state toggleable option.
|
||||
*/
|
||||
data class SwitchPreference(
|
||||
val pref: PreferenceData<Boolean>,
|
||||
val preference: PreferenceData<Boolean>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>()
|
||||
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that provides a slider to select an integer number.
|
||||
*/
|
||||
data class SliderPreference(
|
||||
val value: Int,
|
||||
val min: Int = 0,
|
||||
val max: Int,
|
||||
override val title: String = "",
|
||||
override val title: String,
|
||||
val valueRange: IntProgression = 0..1,
|
||||
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
|
||||
) : PreferenceItem<Int>()
|
||||
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
|
||||
) : PreferenceItem<Int>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class ListPreference<T>(
|
||||
val pref: PreferenceData<T>,
|
||||
val preference: PreferenceData<T>,
|
||||
val entries: ImmutableMap<T, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<T, String>,
|
||||
override val onValueChanged: suspend (value: T) -> Boolean = { true },
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
||||
internal fun internalSet(value: Any) = preference.set(value as T)
|
||||
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
|
||||
@@ -87,15 +89,14 @@ sealed class Preference {
|
||||
*/
|
||||
data class BasicListPreference(
|
||||
val value: String,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e -> subtitle?.format(e[v]) },
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
@@ -103,52 +104,51 @@ sealed class Preference {
|
||||
* Multiple entries can be selected at the same time.
|
||||
*/
|
||||
data class MultiSelectListPreference(
|
||||
val pref: PreferenceData<Set<String>>,
|
||||
val preference: PreferenceData<Set<String>>,
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (
|
||||
value: Set<String>,
|
||||
entries: ImmutableMap<String, String>,
|
||||
) -> String? = { v, e ->
|
||||
val combined = remember(v) {
|
||||
v.map { e[it] }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.joinToString()
|
||||
} ?: stringResource(MR.strings.none)
|
||||
subtitle?.format(combined)
|
||||
},
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? =
|
||||
{ v, e ->
|
||||
val combined = remember(v, e) {
|
||||
v.mapNotNull { e[it] }
|
||||
.joinToString()
|
||||
.takeUnless { it.isBlank() }
|
||||
}
|
||||
?: stringResource(MR.strings.none)
|
||||
subtitle?.format(combined)
|
||||
},
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
||||
val entries: ImmutableMap<String, String>,
|
||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<String>>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
*/
|
||||
data class EditTextPreference(
|
||||
val pref: PreferenceData<String>,
|
||||
val preference: PreferenceData<String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>() {
|
||||
override val icon: ImageVector? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] for individual tracker.
|
||||
*/
|
||||
data class TrackerPreference(
|
||||
val tracker: Tracker,
|
||||
override val title: String,
|
||||
val login: () -> Unit,
|
||||
val logout: () -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
override val title: String = ""
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
data class InfoPreference(
|
||||
@@ -157,17 +157,17 @@ sealed class Preference {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: String) -> Boolean = { true }
|
||||
}
|
||||
|
||||
data class CustomPreference(
|
||||
override val title: String,
|
||||
val content: @Composable (PreferenceItem<String>) -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
val content: @Composable () -> Unit,
|
||||
) : PreferenceItem<Unit>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,8 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -12,16 +14,20 @@ import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.InfoWidget
|
||||
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TitleFontSize
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.components.BaseSliderItem
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||
@@ -60,7 +66,7 @@ internal fun PreferenceItem(
|
||||
) {
|
||||
when (item) {
|
||||
is Preference.PreferenceItem.SwitchPreference -> {
|
||||
val value by item.pref.collectAsState()
|
||||
val value by item.preference.collectAsState()
|
||||
SwitchPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
@@ -69,29 +75,33 @@ internal fun PreferenceItem(
|
||||
onCheckedChanged = { newValue ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValue)) {
|
||||
item.pref.set(newValue)
|
||||
item.preference.set(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.SliderPreference -> {
|
||||
// TODO: use different composable?
|
||||
SliderItem(
|
||||
BaseSliderItem(
|
||||
label = item.title,
|
||||
min = item.min,
|
||||
max = item.max,
|
||||
value = item.value,
|
||||
valueRange = item.valueRange,
|
||||
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
|
||||
steps = item.steps,
|
||||
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
|
||||
onChange = {
|
||||
scope.launch {
|
||||
item.onValueChanged(it)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(
|
||||
horizontal = PrefsHorizontalPadding,
|
||||
vertical = PrefsVerticalPadding,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.ListPreference<*> -> {
|
||||
val value by item.pref.collectAsState()
|
||||
val value by item.preference.collectAsState()
|
||||
ListPreferenceWidget(
|
||||
value = value,
|
||||
title = item.title,
|
||||
@@ -118,14 +128,14 @@ internal fun PreferenceItem(
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
val values by item.preference.collectAsState()
|
||||
MultiSelectListPreferenceWidget(
|
||||
preference = item,
|
||||
values = values,
|
||||
onValuesChange = { newValues ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValues)) {
|
||||
item.pref.set(newValues.toMutableSet())
|
||||
item.preference.set(newValues.toMutableSet())
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -140,7 +150,7 @@ internal fun PreferenceItem(
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.EditTextPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
val values by item.preference.collectAsState()
|
||||
EditTextPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
@@ -148,7 +158,7 @@ internal fun PreferenceItem(
|
||||
value = values,
|
||||
onConfirm = {
|
||||
val accepted = item.onValueChanged(it)
|
||||
if (accepted) item.pref.set(it)
|
||||
if (accepted) item.preference.set(it)
|
||||
accepted
|
||||
},
|
||||
)
|
||||
@@ -167,7 +177,7 @@ internal fun PreferenceItem(
|
||||
InfoWidget(text = item.title)
|
||||
}
|
||||
is Preference.PreferenceItem.CustomPreference -> {
|
||||
item.content(item)
|
||||
item.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -47,8 +47,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
@@ -61,6 +61,7 @@ import logcat.LogPriority
|
||||
import okhttp3.Headers
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -96,7 +97,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = networkPreferences.verboseLogging(),
|
||||
preference = networkPreferences.verboseLogging(),
|
||||
title = stringResource(MR.strings.pref_verbose_logging),
|
||||
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
@@ -235,8 +236,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = networkPreferences.dohProvider(),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
preference = networkPreferences.dohProvider(),
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
@@ -252,13 +252,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
PREF_DOH_NJALLA to "Njalla",
|
||||
PREF_DOH_SHECAN to "Shecan",
|
||||
),
|
||||
title = stringResource(MR.strings.pref_dns_over_https),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
pref = userAgentPref,
|
||||
preference = userAgentPref,
|
||||
title = stringResource(MR.strings.pref_user_agent_string),
|
||||
onValueChanged = {
|
||||
try {
|
||||
@@ -334,6 +335,31 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_reader),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = basePreferences.hardwareBitmapThreshold(),
|
||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||
.mapIndexed { index, option ->
|
||||
val display = if (index == 0) {
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
||||
} else {
|
||||
option.toString()
|
||||
}
|
||||
option to display
|
||||
}
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||
subtitleProvider = { value, options ->
|
||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||
},
|
||||
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
|
||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
|
||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_display_profile),
|
||||
subtitle = basePreferences.displayProfile().get(),
|
||||
@@ -382,19 +408,19 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_extensions),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = extensionInstallerPref,
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
preference = extensionInstallerPref,
|
||||
entries = extensionInstallerPref.entries
|
||||
.filter {
|
||||
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||
if (isPreviewBuildType || isDevFlavor) {
|
||||
true
|
||||
} else {
|
||||
if (isReleaseBuildType) {
|
||||
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
onValueChanged = {
|
||||
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
|
||||
!context.isShizukuInstalled
|
||||
|
@@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
}
|
||||
},
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = amoledPref,
|
||||
preference = amoledPref,
|
||||
title = stringResource(MR.strings.pref_dark_theme_pure_black),
|
||||
enabled = themeMode != ThemeMode.LIGHT,
|
||||
onValueChanged = {
|
||||
@@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
onClick = { navigator.push(AppLanguageScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.tabletUiMode(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
preference = uiPreferences.tabletUiMode(),
|
||||
entries = TabletUiMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_tablet_ui_mode),
|
||||
onValueChanged = {
|
||||
context.toast(MR.strings.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
preference = uiPreferences.dateFormat(),
|
||||
entries = DateFormats
|
||||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_date_format),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
preference = uiPreferences.relativeTime(),
|
||||
title = stringResource(MR.strings.pref_relative_format),
|
||||
subtitle = stringResource(
|
||||
MR.strings.pref_relative_format_summary,
|
||||
|
@@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.hideInLibraryItems(),
|
||||
preference = sourcePreferences.hideInLibraryItems(),
|
||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
@@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.showNsfwSource(),
|
||||
preference = sourcePreferences.showNsfwSource(),
|
||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
||||
subtitle = stringResource(MR.strings.requires_app_restart),
|
||||
onValueChanged = {
|
||||
|
@@ -7,7 +7,9 @@ import android.net.Uri
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -15,6 +17,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
@@ -22,12 +26,15 @@ import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@@ -45,10 +52,14 @@ import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.export.LibraryExporter
|
||||
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
@@ -57,8 +68,11 @@ import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.backup.service.BackupPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -95,6 +109,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
getExportGroup(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -239,8 +254,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
|
||||
// Automatic backups
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.backupInterval(),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
preference = backupPreferences.backupInterval(),
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
6 to stringResource(MR.strings.update_6hour),
|
||||
@@ -249,6 +263,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||
48 to stringResource(MR.strings.update_48hour),
|
||||
168 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_backup_interval),
|
||||
onValueChanged = {
|
||||
BackupCreateJob.setupTask(context, it)
|
||||
true
|
||||
@@ -306,10 +321,147 @@ object SettingsDataScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoClearChapterCache(),
|
||||
preference = libraryPreferences.autoClearChapterCache(),
|
||||
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExportGroup(): Preference.PreferenceGroup {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var exportOptions by remember {
|
||||
mutableStateOf(
|
||||
ExportOptions(
|
||||
includeTitle = true,
|
||||
includeAuthor = true,
|
||||
includeArtist = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val getFavorites = remember { Injekt.get<GetFavorites>() }
|
||||
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
|
||||
LaunchedEffect(Unit) {
|
||||
favorites = getFavorites.await()
|
||||
}
|
||||
|
||||
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("text/csv"),
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
scope.launch {
|
||||
LibraryExporter.exportToCsv(
|
||||
context = context,
|
||||
uri = it,
|
||||
favorites = favorites,
|
||||
options = exportOptions,
|
||||
onExportComplete = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
context.toast(MR.strings.library_exported)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ColumnSelectionDialog(
|
||||
options = exportOptions,
|
||||
onConfirm = { options ->
|
||||
exportOptions = options
|
||||
saveFileLauncher.launch("mihon_library.csv")
|
||||
},
|
||||
onDismissRequest = { showDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.export),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.library_list),
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnSelectionDialog(
|
||||
options: ExportOptions,
|
||||
onConfirm: (ExportOptions) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
var titleSelected by remember { mutableStateOf(options.includeTitle) }
|
||||
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
|
||||
var artistSelected by remember { mutableStateOf(options.includeArtist) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = titleSelected,
|
||||
onCheckedChange = { checked ->
|
||||
titleSelected = checked
|
||||
if (!checked) {
|
||||
authorSelected = false
|
||||
artistSelected = false
|
||||
}
|
||||
},
|
||||
)
|
||||
Text(text = stringResource(MR.strings.title))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = authorSelected,
|
||||
onCheckedChange = { authorSelected = it },
|
||||
enabled = titleSelected,
|
||||
)
|
||||
Text(text = stringResource(MR.strings.author))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = artistSelected,
|
||||
onCheckedChange = { artistSelected = it },
|
||||
enabled = titleSelected,
|
||||
)
|
||||
Text(text = stringResource(MR.strings.artist))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm(
|
||||
ExportOptions(
|
||||
includeTitle = titleSelected,
|
||||
includeAuthor = authorSelected,
|
||||
includeArtist = artistSelected,
|
||||
),
|
||||
)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -39,15 +39,15 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.downloadOnlyOverWifi(),
|
||||
preference = downloadPreferences.downloadOnlyOverWifi(),
|
||||
title = stringResource(MR.strings.connected_to_wifi),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.saveChaptersAsCBZ(),
|
||||
preference = downloadPreferences.saveChaptersAsCBZ(),
|
||||
title = stringResource(MR.strings.save_chapter_as_cbz),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.splitTallImages(),
|
||||
preference = downloadPreferences.splitTallImages(),
|
||||
title = stringResource(MR.strings.split_tall_images),
|
||||
subtitle = stringResource(MR.strings.split_tall_images_summary),
|
||||
),
|
||||
@@ -72,12 +72,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_delete_chapters),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
preference = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.removeAfterReadSlots(),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
preference = downloadPreferences.removeAfterReadSlots(),
|
||||
entries = persistentMapOf(
|
||||
-1 to stringResource(MR.strings.disabled),
|
||||
0 to stringResource(MR.strings.last_read_chapter),
|
||||
@@ -86,9 +85,10 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
3 to stringResource(MR.strings.fourth_to_last),
|
||||
4 to stringResource(MR.strings.fifth_to_last),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_remove_after_read),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeBookmarkedChapters(),
|
||||
preference = downloadPreferences.removeBookmarkedChapters(),
|
||||
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
|
||||
),
|
||||
getExcludedCategoriesPreference(
|
||||
@@ -105,11 +105,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = downloadPreferences.removeExcludeCategories(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
||||
preference = downloadPreferences.removeExcludeCategories(),
|
||||
entries = categories()
|
||||
.associate { it.id.toString() to it.visualName }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_remove_exclude_categories),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,11 +149,11 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_auto_download),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewChaptersPref,
|
||||
preference = downloadNewChaptersPref,
|
||||
title = stringResource(MR.strings.pref_download_new),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewUnreadChaptersOnlyPref,
|
||||
preference = downloadNewUnreadChaptersOnlyPref,
|
||||
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
|
||||
enabled = downloadNewChapters,
|
||||
),
|
||||
@@ -164,8 +164,8 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
onClick = { showDialog = true },
|
||||
enabled = downloadNewChapters,
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -179,8 +179,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.download_ahead),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
preference = downloadPreferences.autoDownloadWhileReading(),
|
||||
entries = listOf(0, 2, 3, 5, 10)
|
||||
.associateWith {
|
||||
if (it == 0) {
|
||||
@@ -190,6 +189,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
}
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.auto_download_while_reading),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
|
||||
),
|
||||
|
@@ -35,6 +35,8 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_EXISTING
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MARK_DUPLICATE_CHAPTER_READ_NEW
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@@ -57,7 +59,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
return listOf(
|
||||
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),
|
||||
getGlobalUpdateGroup(allCategories, libraryPreferences),
|
||||
getChapterSwipeActionsGroup(libraryPreferences),
|
||||
getBehaviorGroup(libraryPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,12 +91,12 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { navigator.push(CategoryScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.defaultCategory(),
|
||||
title = stringResource(MR.strings.default_category),
|
||||
preference = libraryPreferences.defaultCategory(),
|
||||
entries = ids.zip(labels).toMap().toImmutableMap(),
|
||||
title = stringResource(MR.strings.default_category),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||
preference = libraryPreferences.categorizedDisplaySettings(),
|
||||
title = stringResource(MR.strings.categorized_display_settings),
|
||||
onValueChanged = {
|
||||
if (!it) {
|
||||
@@ -146,8 +148,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_library_update),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = autoUpdateIntervalPref,
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
preference = autoUpdateIntervalPref,
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.update_never),
|
||||
12 to stringResource(MR.strings.update_12hour),
|
||||
@@ -156,21 +157,22 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
72 to stringResource(MR.strings.update_72hour),
|
||||
168 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_interval),
|
||||
onValueChanged = {
|
||||
LibraryUpdateJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||
enabled = autoUpdateInterval > 0,
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
preference = libraryPreferences.autoUpdateDeviceRestrictions(),
|
||||
entries = persistentMapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(MR.strings.charging),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_restriction),
|
||||
subtitle = stringResource(MR.strings.restrictions),
|
||||
enabled = autoUpdateInterval > 0,
|
||||
onValueChanged = {
|
||||
// Post to event looper to allow the preference to be updated.
|
||||
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
|
||||
@@ -187,22 +189,22 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
onClick = { showCategoriesDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoUpdateMetadata(),
|
||||
preference = libraryPreferences.autoUpdateMetadata(),
|
||||
title = stringResource(MR.strings.pref_library_update_refresh_metadata),
|
||||
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
preference = libraryPreferences.autoUpdateMangaRestrictions(),
|
||||
entries = persistentMapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
|
||||
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_library_update_smart_update),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.newShowUpdatesCount(),
|
||||
preference = libraryPreferences.newShowUpdatesCount(),
|
||||
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
|
||||
),
|
||||
),
|
||||
@@ -210,15 +212,14 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getChapterSwipeActionsGroup(
|
||||
private fun getBehaviorGroup(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_chapter_swipe),
|
||||
title = stringResource(MR.strings.pref_behavior),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeToStartAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
preference = libraryPreferences.swipeToStartAction(),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -229,10 +230,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_start),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.swipeToEndAction(),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
preference = libraryPreferences.swipeToEndAction(),
|
||||
entries = persistentMapOf(
|
||||
LibraryPreferences.ChapterSwipeAction.Disabled to
|
||||
stringResource(MR.strings.disabled),
|
||||
@@ -243,6 +244,17 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
LibraryPreferences.ChapterSwipeAction.Download to
|
||||
stringResource(MR.strings.action_download),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_chapter_swipe_end),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = libraryPreferences.markDuplicateReadChapterAsRead(),
|
||||
entries = persistentMapOf(
|
||||
MARK_DUPLICATE_CHAPTER_READ_EXISTING to
|
||||
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_existing),
|
||||
MARK_DUPLICATE_CHAPTER_READ_NEW to
|
||||
stringResource(MR.strings.pref_mark_duplicate_read_chapter_read_new),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.defaultReadingMode(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
preference = readerPref.defaultReadingMode(),
|
||||
entries = ReadingMode.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.doubleTapAnimSpeed(),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
preference = readerPref.doubleTapAnimSpeed(),
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.double_tap_anim_speed_0),
|
||||
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
|
||||
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_double_tap_anim_speed),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showReadingMode(),
|
||||
preference = readerPref.showReadingMode(),
|
||||
title = stringResource(MR.strings.pref_show_reading_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showNavigationOverlayOnStart(),
|
||||
preference = readerPref.showNavigationOverlayOnStart(),
|
||||
title = stringResource(MR.strings.pref_show_navigation_mode),
|
||||
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.pageTransitions(),
|
||||
preference = readerPref.pageTransitions(),
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
),
|
||||
getDisplayGroup(readerPreferences = readerPref),
|
||||
@@ -80,39 +80,39 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.defaultOrientationType(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
preference = readerPreferences.defaultOrientationType(),
|
||||
entries = ReaderOrientation.entries.drop(1)
|
||||
.associate { it.flagValue to stringResource(it.stringRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_rotation_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerTheme(),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
preference = readerPreferences.readerTheme(),
|
||||
entries = persistentMapOf(
|
||||
1 to stringResource(MR.strings.black_background),
|
||||
2 to stringResource(MR.strings.gray_background),
|
||||
0 to stringResource(MR.strings.white_background),
|
||||
3 to stringResource(MR.strings.automatic_background),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_reader_theme),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = fullscreenPref,
|
||||
preference = fullscreenPref,
|
||||
title = stringResource(MR.strings.pref_fullscreen),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cutoutShort(),
|
||||
preference = readerPreferences.cutoutShort(),
|
||||
title = stringResource(MR.strings.pref_cutout_short),
|
||||
enabled = fullscreen &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.keepScreenOn(),
|
||||
preference = readerPreferences.keepScreenOn(),
|
||||
title = stringResource(MR.strings.pref_keep_screen_on),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.showPageNumber(),
|
||||
preference = readerPreferences.showPageNumber(),
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
),
|
||||
@@ -135,43 +135,41 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = "E-Ink",
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.flashOnPageChange(),
|
||||
preference = readerPreferences.flashOnPageChange(),
|
||||
title = stringResource(MR.strings.pref_flash_page),
|
||||
subtitle = stringResource(MR.strings.pref_flash_page_summ),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
min = 1,
|
||||
max = 15,
|
||||
valueRange = 1..15,
|
||||
title = stringResource(MR.strings.pref_flash_duration),
|
||||
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
|
||||
true
|
||||
},
|
||||
enabled = flashPageState,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = flashInterval,
|
||||
min = 1,
|
||||
max = 10,
|
||||
valueRange = 1..10,
|
||||
title = stringResource(MR.strings.pref_flash_page_interval),
|
||||
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
enabled = flashPageState,
|
||||
onValueChanged = {
|
||||
flashIntervalPref.set(it)
|
||||
true
|
||||
},
|
||||
enabled = flashPageState,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = flashColorPref,
|
||||
title = stringResource(MR.strings.pref_flash_with),
|
||||
preference = flashColorPref,
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
|
||||
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
|
||||
ReaderPreferences.FlashColor.WHITE_BLACK
|
||||
to stringResource(MR.strings.pref_flash_style_white_black),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_flash_with),
|
||||
enabled = flashPageState,
|
||||
),
|
||||
),
|
||||
@@ -184,19 +182,19 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_category_reading),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipRead(),
|
||||
preference = readerPreferences.skipRead(),
|
||||
title = stringResource(MR.strings.pref_skip_read_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipFiltered(),
|
||||
preference = readerPreferences.skipFiltered(),
|
||||
title = stringResource(MR.strings.pref_skip_filtered_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.skipDupe(),
|
||||
preference = readerPreferences.skipDupe(),
|
||||
title = stringResource(MR.strings.pref_skip_dupe_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.alwaysShowChapterTransition(),
|
||||
preference = readerPreferences.alwaysShowChapterTransition(),
|
||||
title = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
),
|
||||
),
|
||||
@@ -219,16 +217,15 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pager_viewer),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
preference = navModePref,
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.pagerNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
preference = readerPreferences.pagerNavInverted(),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -237,40 +234,41 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = imageScaleTypePref,
|
||||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
preference = imageScaleTypePref,
|
||||
entries = ReaderPreferences.ImageScaleType
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_image_scale_type),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.zoomStart(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
preference = readerPreferences.zoomStart(),
|
||||
entries = ReaderPreferences.ZoomStart
|
||||
.mapIndexed { index, it -> index + 1 to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_zoom_start),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBorders(),
|
||||
preference = readerPreferences.cropBorders(),
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.landscapeZoom(),
|
||||
preference = readerPreferences.landscapeZoom(),
|
||||
title = stringResource(MR.strings.pref_landscape_zoom),
|
||||
enabled = imageScaleType == 1,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.navigateToPan(),
|
||||
preference = readerPreferences.navigateToPan(),
|
||||
title = stringResource(MR.strings.pref_navigate_pan),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
preference = dualPageSplitPref,
|
||||
title = stringResource(MR.strings.pref_dual_page_split),
|
||||
onValueChanged = {
|
||||
rotateToFitPref.set(false)
|
||||
@@ -278,13 +276,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertPaged(),
|
||||
preference = readerPreferences.dualPageInvertPaged(),
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = rotateToFitPref,
|
||||
preference = rotateToFitPref,
|
||||
title = stringResource(MR.strings.pref_page_rotate),
|
||||
onValueChanged = {
|
||||
dualPageSplitPref.set(false)
|
||||
@@ -292,7 +290,7 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageRotateToFitInvert(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvert(),
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
@@ -318,16 +316,15 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.webtoon_viewer),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
preference = navModePref,
|
||||
entries = ReaderPreferences.TapZones
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_viewer_nav),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonNavInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
preference = readerPreferences.webtoonNavInverted(),
|
||||
entries = persistentListOf(
|
||||
ReaderPreferences.TappingInvertMode.NONE,
|
||||
ReaderPreferences.TappingInvertMode.HORIZONTAL,
|
||||
@@ -336,35 +333,37 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
)
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let {
|
||||
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
|
||||
},
|
||||
title = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
subtitle = numberFormat.format(webtoonSidePadding / 100f),
|
||||
min = ReaderPreferences.WEBTOON_PADDING_MIN,
|
||||
max = ReaderPreferences.WEBTOON_PADDING_MAX,
|
||||
onValueChanged = {
|
||||
webtoonSidePaddingPref.set(it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerHideThreshold(),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
preference = readerPreferences.readerHideThreshold(),
|
||||
entries = persistentMapOf(
|
||||
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
|
||||
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
|
||||
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
|
||||
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
|
||||
),
|
||||
title = stringResource(MR.strings.pref_hide_threshold),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBordersWebtoon(),
|
||||
preference = readerPreferences.cropBordersWebtoon(),
|
||||
title = stringResource(MR.strings.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
preference = dualPageSplitPref,
|
||||
title = stringResource(MR.strings.pref_dual_page_split),
|
||||
onValueChanged = {
|
||||
rotateToFitPref.set(false)
|
||||
@@ -372,13 +371,13 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageInvertWebtoon(),
|
||||
title = stringResource(MR.strings.pref_dual_page_invert),
|
||||
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = rotateToFitPref,
|
||||
preference = rotateToFitPref,
|
||||
title = stringResource(MR.strings.pref_page_rotate),
|
||||
onValueChanged = {
|
||||
dualPageSplitPref.set(false)
|
||||
@@ -386,16 +385,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
|
||||
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
|
||||
title = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
enabled = rotateToFit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDisableZoomOut(),
|
||||
preference = readerPreferences.webtoonDisableZoomOut(),
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
),
|
||||
@@ -410,11 +409,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readWithVolumeKeysPref,
|
||||
preference = readWithVolumeKeysPref,
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithVolumeKeysInverted(),
|
||||
preference = readerPreferences.readWithVolumeKeysInverted(),
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
@@ -428,11 +427,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_reader_actions),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithLongTap(),
|
||||
preference = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.folderPerManga(),
|
||||
preference = readerPreferences.folderPerManga(),
|
||||
title = stringResource(MR.strings.pref_create_folder_per_manga),
|
||||
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
|
||||
),
|
||||
|
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||
import eu.kanade.tachiyomi.util.system.telemetryIncluded
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -31,10 +32,11 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
|
||||
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
|
||||
return listOf(
|
||||
getSecurityGroup(securityPreferences),
|
||||
getFirebaseGroup(privacyPreferences),
|
||||
)
|
||||
return buildList(2) {
|
||||
add(getSecurityGroup(securityPreferences))
|
||||
if (!telemetryIncluded) return@buildList
|
||||
add(getFirebaseGroup(privacyPreferences))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -50,7 +52,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_security),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = useAuthPref,
|
||||
preference = useAuthPref,
|
||||
title = stringResource(MR.strings.lock_with_biometrics),
|
||||
enabled = authSupported,
|
||||
onValueChanged = {
|
||||
@@ -60,9 +62,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.lockAppAfter(),
|
||||
title = stringResource(MR.strings.lock_when_idle),
|
||||
enabled = authSupported && useAuth,
|
||||
preference = securityPreferences.lockAppAfter(),
|
||||
entries = LockAfterValues
|
||||
.associateWith {
|
||||
when (it) {
|
||||
@@ -72,6 +72,8 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
}
|
||||
}
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.lock_when_idle),
|
||||
enabled = authSupported && useAuth,
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.stringResource(MR.strings.lock_when_idle),
|
||||
@@ -80,15 +82,15 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
),
|
||||
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = securityPreferences.hideNotificationContent(),
|
||||
preference = securityPreferences.hideNotificationContent(),
|
||||
title = stringResource(MR.strings.hide_notification_content),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.secureScreen(),
|
||||
title = stringResource(MR.strings.secure_screen),
|
||||
preference = securityPreferences.secureScreen(),
|
||||
entries = SecurityPreferences.SecureScreenMode.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(MR.strings.secure_screen),
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
|
||||
),
|
||||
@@ -103,12 +105,12 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_firebase),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = privacyPreferences.crashlytics(),
|
||||
preference = privacyPreferences.crashlytics(),
|
||||
title = stringResource(MR.strings.onboarding_permission_crashlytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = privacyPreferences.analytics(),
|
||||
preference = privacyPreferences.analytics(),
|
||||
title = stringResource(MR.strings.onboarding_permission_analytics),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
|
||||
),
|
||||
|
@@ -125,51 +125,45 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = trackPreferences.autoUpdateTrack(),
|
||||
preference = trackPreferences.autoUpdateTrack(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_sync),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
|
||||
entries = AutoTrackState.entries
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toPersistentMap(),
|
||||
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.myAnimeList.name,
|
||||
tracker = trackerManager.myAnimeList,
|
||||
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.aniList.name,
|
||||
tracker = trackerManager.aniList,
|
||||
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.aniList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.kitsu.name,
|
||||
tracker = trackerManager.kitsu,
|
||||
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.mangaUpdates.name,
|
||||
tracker = trackerManager.mangaUpdates,
|
||||
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.shikimori.name,
|
||||
tracker = trackerManager.shikimori,
|
||||
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = trackerManager.bangumi.name,
|
||||
tracker = trackerManager.bangumi,
|
||||
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
|
||||
@@ -183,7 +177,6 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
enhancedTrackers.first
|
||||
.map { service ->
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
title = service.name,
|
||||
tracker = service,
|
||||
login = { (service as EnhancedTracker).loginNoop() },
|
||||
logout = service::logout,
|
||||
|
@@ -35,7 +35,9 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.updaterEnabled
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
@@ -97,7 +99,7 @@ object AboutScreen : Screen() {
|
||||
)
|
||||
}
|
||||
|
||||
if (BuildConfig.INCLUDE_UPDATER) {
|
||||
if (updaterEnabled) {
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(MR.strings.check_for_updates),
|
||||
@@ -121,7 +123,7 @@ object AboutScreen : Screen() {
|
||||
versionName = result.release.version,
|
||||
changelogInfo = result.release.info,
|
||||
releaseLink = result.release.releaseLink,
|
||||
downloadLink = result.release.getDownloadLink(),
|
||||
downloadLink = result.release.downloadLink,
|
||||
)
|
||||
navigator.push(updateScreen)
|
||||
},
|
||||
@@ -245,7 +247,7 @@ object AboutScreen : Screen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
BuildConfig.PREVIEW -> {
|
||||
isPreviewBuildType -> {
|
||||
"Beta r${BuildConfig.COMMIT_COUNT}".let {
|
||||
if (withBuildDate) {
|
||||
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -36,12 +37,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
if (customBrightness) {
|
||||
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
min = -75,
|
||||
max = 100,
|
||||
value = customBrightnessValue,
|
||||
valueText = customBrightnessValue.toString(),
|
||||
valueRange = -75..100,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,48 +54,52 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
if (colorFilter) {
|
||||
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.red,
|
||||
valueText = colorFilterValue.red.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
onChange = { newRValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newRValue, RED_MASK, 16)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.green,
|
||||
valueText = colorFilterValue.green.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
onChange = { newGValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newGValue, GREEN_MASK, 8)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.blue,
|
||||
valueText = colorFilterValue.blue.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
onChange = { newBValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newBValue, BLUE_MASK, 0)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
max = 255,
|
||||
value = colorFilterValue.alpha,
|
||||
valueText = colorFilterValue.alpha.toString(),
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
onChange = { newAValue ->
|
||||
screenModel.preferences.colorFilterValue().getAndSet {
|
||||
getColorValue(it, newAValue, ALPHA_MASK, 24)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -97,21 +98,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
if (flashPageState) {
|
||||
SliderItem(
|
||||
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
|
||||
valueRange = 1..15,
|
||||
label = stringResource(MR.strings.pref_flash_duration),
|
||||
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
|
||||
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
|
||||
min = 1,
|
||||
max = 15,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SliderItem(
|
||||
value = flashInterval,
|
||||
valueRange = 1..10,
|
||||
label = stringResource(MR.strings.pref_flash_page_interval),
|
||||
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
|
||||
onChange = {
|
||||
flashIntervalPref.set(it)
|
||||
},
|
||||
min = 1,
|
||||
max = 10,
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
SettingsChipRow(MR.strings.pref_flash_with) {
|
||||
flashColors.map { (labelRes, value) ->
|
||||
|
@@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -152,14 +153,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
min = ReaderPreferences.WEBTOON_PADDING_MIN,
|
||||
max = ReaderPreferences.WEBTOON_PADDING_MAX,
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
valueText = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onChange = {
|
||||
screenModel.preferences.webtoonSidePadding().set(it)
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
|
@@ -13,6 +13,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.NordColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
|
||||
@@ -79,6 +80,7 @@ private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||
AppTheme.LAVENDER to LavenderColorScheme,
|
||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||
AppTheme.MONOCHROME to MonochromeColorScheme,
|
||||
AppTheme.NORD to NordColorScheme,
|
||||
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
|
||||
AppTheme.TAKO to TakoColorScheme,
|
||||
|
@@ -0,0 +1,84 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal object MonochromeColorScheme : BaseColorScheme() {
|
||||
|
||||
override val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFFFFFFFF),
|
||||
onPrimary = Color(0xFF000000),
|
||||
primaryContainer = Color(0xFFFFFFFF),
|
||||
onPrimaryContainer = Color(0xFF000000),
|
||||
secondary = Color(0xFFFFFFFF),
|
||||
onSecondary = Color(0xFF000000),
|
||||
secondaryContainer = Color(0xFF777777),
|
||||
onSecondaryContainer = Color(0xFF000000),
|
||||
tertiary = Color(0xFF777777),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFFFFFFFF),
|
||||
onTertiaryContainer = Color(0xFF000000),
|
||||
error = Color(0xFFFFFFFF),
|
||||
onError = Color(0xFF000000),
|
||||
errorContainer = Color(0xFFFFFFFF),
|
||||
onErrorContainer = Color(0xFF000000),
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFFFFFFF),
|
||||
surface = Color(0xFF000000),
|
||||
onSurface = Color(0xFFFFFFFF),
|
||||
surfaceVariant = Color(0xFF000000),
|
||||
onSurfaceVariant = Color(0xFFFFFFFF),
|
||||
outline = Color(0xFFFFFFFF),
|
||||
outlineVariant = Color(0xFFFFFFFF),
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFFFFFFFF),
|
||||
inverseOnSurface = Color(0xFF000000),
|
||||
inversePrimary = Color(0xFF000000),
|
||||
surfaceDim = Color(0xFF000000),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFF000000),
|
||||
surfaceContainerLow = Color(0xFF000000),
|
||||
surfaceContainer = Color(0xFF000000),
|
||||
surfaceContainerHigh = Color(0xFF000000),
|
||||
surfaceContainerHighest = Color(0xFF000000),
|
||||
)
|
||||
|
||||
override val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF000000),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFF000000),
|
||||
onPrimaryContainer = Color(0xFFFFFFFF),
|
||||
secondary = Color(0xFF000000),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFF888888),
|
||||
onSecondaryContainer = Color(0xFFFFFFFF),
|
||||
tertiary = Color(0xFF888888),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFF000000),
|
||||
onTertiaryContainer = Color(0xFFFFFFFF),
|
||||
error = Color(0xFF000000),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFF000000),
|
||||
onErrorContainer = Color(0xFFFFFFFF),
|
||||
background = Color(0xFFFFFFFF),
|
||||
onBackground = Color(0xFF000000),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFF000000),
|
||||
surfaceVariant = Color(0xFFFFFFFF),
|
||||
onSurfaceVariant = Color(0xFF000000),
|
||||
outline = Color(0xFF000000),
|
||||
outlineVariant = Color(0xFF000000),
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFF000000),
|
||||
inverseOnSurface = Color(0xFFFFFFFF),
|
||||
inversePrimary = Color(0xFFFFFFFF),
|
||||
surfaceDim = Color(0xFFFFFFFF),
|
||||
surfaceBright = Color(0xFFFFFFFF),
|
||||
surfaceContainerLowest = Color(0xFFFFFFFF),
|
||||
surfaceContainerLow = Color(0xFFFFFFFF),
|
||||
surfaceContainer = Color(0xFFFFFFFF),
|
||||
surfaceContainerHigh = Color(0xFFFFFFFF),
|
||||
surfaceContainerHighest = Color(0xFFFFFFFF),
|
||||
)
|
||||
}
|
@@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
@@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
|
||||
onOpenInBrowser: (TrackItem) -> Unit,
|
||||
onRemoved: (TrackItem) -> Unit,
|
||||
onCopyLink: (TrackItem) -> Unit,
|
||||
onTogglePrivate: (TrackItem) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
|
||||
if (item.track != null) {
|
||||
val supportsScoring = item.tracker.getScoreList().isNotEmpty()
|
||||
val supportsReadingDates = item.tracker.supportsReadingDates
|
||||
val supportsPrivate = item.tracker.supportsPrivateTracking
|
||||
TrackInfoItem(
|
||||
title = item.track.title,
|
||||
tracker = item.tracker,
|
||||
@@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
|
||||
onOpenInBrowser = { onOpenInBrowser(item) },
|
||||
onRemoved = { onRemoved(item) },
|
||||
onCopyLink = { onCopyLink(item) },
|
||||
private = item.track.private,
|
||||
onTogglePrivate = { onTogglePrivate(item) }
|
||||
.takeIf { supportsPrivate },
|
||||
)
|
||||
} else {
|
||||
TrackInfoItemEmpty(
|
||||
@@ -144,17 +154,37 @@ private fun TrackInfoItem(
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
onCopyLink: () -> Unit,
|
||||
private: Boolean,
|
||||
onTogglePrivate: (() -> Unit)?,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(
|
||||
tracker = tracker,
|
||||
onClick = onOpenInBrowser,
|
||||
onLongClick = onCopyLink,
|
||||
)
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (private) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.absoluteOffset(x = (-5).dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.VisibilityOff,
|
||||
contentDescription = stringResource(MR.strings.tracked_privately),
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
TrackLogoIcon(
|
||||
tracker = tracker,
|
||||
onClick = onOpenInBrowser,
|
||||
onLongClick = onCopyLink,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
@@ -181,6 +211,8 @@ private fun TrackInfoItem(
|
||||
onOpenInBrowser = onOpenInBrowser,
|
||||
onRemoved = onRemoved,
|
||||
onCopyLink = onCopyLink,
|
||||
private = private,
|
||||
onTogglePrivate = onTogglePrivate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRemoved: () -> Unit,
|
||||
onCopyLink: () -> Unit,
|
||||
private: Boolean,
|
||||
onTogglePrivate: (() -> Unit)?,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
|
||||
@@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
if (onTogglePrivate != null) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (private) {
|
||||
MR.strings.action_toggle_private_off
|
||||
} else {
|
||||
MR.strings.action_toggle_private_on
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onTogglePrivate()
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(MR.strings.action_remove)) },
|
||||
onClick = {
|
||||
|
@@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
remoteUrl = "https://example.com",
|
||||
startDate = 0L,
|
||||
finishDate = 0L,
|
||||
private = false,
|
||||
)
|
||||
private val privateTrack = aTrack.copy(private = true)
|
||||
private val trackItemWithoutTrack = TrackItem(
|
||||
track = null,
|
||||
tracker = DummyTracker(
|
||||
@@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
name = "Example Tracker 2",
|
||||
),
|
||||
)
|
||||
private val trackItemWithPrivateTrack = TrackItem(
|
||||
track = privateTrack,
|
||||
tracker = DummyTracker(
|
||||
id = 2L,
|
||||
name = "Example Tracker 2",
|
||||
),
|
||||
)
|
||||
|
||||
private val trackersWithAndWithoutTrack = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
@@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
private val trackerWithPrivateTracking = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
trackItems = listOf(trackItemWithPrivateTrack),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
onStartDateEdit = {},
|
||||
onEndDateEdit = {},
|
||||
onNewSearch = {},
|
||||
onOpenInBrowser = {},
|
||||
onRemoved = {},
|
||||
onCopyLink = {},
|
||||
onTogglePrivate = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,5 +108,6 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
get() = sequenceOf(
|
||||
trackersWithAndWithoutTrack,
|
||||
noTrackers,
|
||||
trackerWithPrivateTracking,
|
||||
)
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -90,8 +91,9 @@ fun TrackerSearch(
|
||||
queryResult: Result<List<TrackSearch>>?,
|
||||
selected: TrackSearch?,
|
||||
onSelectedChange: (TrackSearch) -> Unit,
|
||||
onConfirmSelection: () -> Unit,
|
||||
onConfirmSelection: (private: Boolean) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
supportsPrivateTracking: Boolean,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -164,15 +166,31 @@ fun TrackerSearch(
|
||||
enter = fadeIn() + slideInVertically { it / 2 },
|
||||
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||
) {
|
||||
Button(
|
||||
onClick = { onConfirmSelection() },
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.padding(MaterialTheme.padding.small)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.fillMaxWidth(),
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_track))
|
||||
Button(
|
||||
onClick = { onConfirmSelection(false) },
|
||||
modifier = Modifier.weight(1f),
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_track))
|
||||
}
|
||||
if (supportsPrivateTracking) {
|
||||
Button(
|
||||
onClick = { onConfirmSelection(true) },
|
||||
elevation = ButtonDefaults.elevatedButtonElevation(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.VisibilityOff,
|
||||
contentDescription = stringResource(MR.strings.action_toggle_private_on),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -286,6 +304,15 @@ private fun SearchResultItem(
|
||||
}
|
||||
},
|
||||
)
|
||||
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
|
||||
Text(
|
||||
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
if (type.isNotBlank()) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(MR.strings.track_type),
|
||||
|
@@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
|
||||
@@ -20,6 +23,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val fullPageWithoutSelected = @Composable {
|
||||
@@ -31,6 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val loading = @Composable {
|
||||
@@ -42,12 +47,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = false,
|
||||
)
|
||||
}
|
||||
private val fullPageWithPrivateTracking = @Composable {
|
||||
val items = someTrackSearches().take(30).toList()
|
||||
TrackerSearch(
|
||||
state = TextFieldState(initialText = "search text"),
|
||||
onDispatchQuery = {},
|
||||
queryResult = Result.success(items),
|
||||
selected = items[1],
|
||||
onSelectedChange = {},
|
||||
onConfirmSelection = {},
|
||||
onDismissRequest = {},
|
||||
supportsPrivateTracking = true,
|
||||
)
|
||||
}
|
||||
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
|
||||
fullPageWithSecondSelected,
|
||||
fullPageWithoutSelected,
|
||||
loading,
|
||||
fullPageWithPrivateTracking,
|
||||
)
|
||||
|
||||
private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
|
||||
@@ -56,6 +76,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
}
|
||||
}
|
||||
|
||||
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
private fun randTrackSearch() = TrackSearch().let {
|
||||
it.id = Random.nextLong()
|
||||
it.manga_id = Random.nextLong()
|
||||
@@ -71,11 +93,17 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
|
||||
it.finished_reading_date = 0L
|
||||
it.tracking_url = "https://example.com/tracker-example"
|
||||
it.cover_url = "https://example.com/cover.png"
|
||||
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
|
||||
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
|
||||
it.summary = lorem((0..40).random()).joinToString()
|
||||
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
|
||||
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
|
||||
it.artists = randomNames()
|
||||
it.authors = randomNames()
|
||||
it
|
||||
}
|
||||
|
||||
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
|
||||
|
||||
private fun lorem(words: Int): Sequence<String> =
|
||||
LoremIpsum(words).values
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package eu.kanade.presentation.webview
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.kevinnzou.web.AccompanistWebViewClient
|
||||
@@ -37,13 +39,18 @@ import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.getHtml
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun WebViewScreenContent(
|
||||
@@ -58,8 +65,11 @@ fun WebViewScreenContent(
|
||||
) {
|
||||
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
|
||||
val navigator = rememberWebViewNavigator()
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val network = remember { Injekt.get<NetworkHelper>() }
|
||||
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
|
||||
|
||||
var currentUrl by remember { mutableStateOf(url) }
|
||||
var showCloudflareHelp by remember { mutableStateOf(false) }
|
||||
@@ -114,6 +124,40 @@ fun WebViewScreenContent(
|
||||
}
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): WebResourceResponse? {
|
||||
return try {
|
||||
val internalRequest = Request.Builder().apply {
|
||||
url(request!!.url.toString())
|
||||
request.requestHeaders.forEach { (key, value) ->
|
||||
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
|
||||
return@forEach
|
||||
}
|
||||
addHeader(key, value)
|
||||
}
|
||||
method(request.method, null)
|
||||
}.build()
|
||||
|
||||
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
|
||||
|
||||
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
|
||||
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
|
||||
|
||||
WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.code,
|
||||
response.message,
|
||||
response.headers.associate { it.first to it.second },
|
||||
response.body.byteStream(),
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
@@ -51,13 +52,14 @@ import kotlinx.coroutines.flow.onEach
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import mihon.core.firebase.FirebaseConfig
|
||||
import mihon.core.migration.Migrator
|
||||
import mihon.core.migration.migrations.migrations
|
||||
import mihon.telemetry.TelemetryConfig
|
||||
import org.conscrypt.Conscrypt
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.widget.WidgetManager
|
||||
@@ -78,7 +80,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
patchInjekt()
|
||||
FirebaseConfig.init(applicationContext)
|
||||
TelemetryConfig.init(applicationContext)
|
||||
|
||||
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||
|
||||
@@ -134,12 +136,20 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
privacyPreferences.analytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setAnalyticsEnabled)
|
||||
.onEach(TelemetryConfig::setAnalyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
privacyPreferences.crashlytics()
|
||||
.changes()
|
||||
.onEach(FirebaseConfig::setCrashlyticsEnabled)
|
||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||
.launchIn(scope)
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().let { preference ->
|
||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||
}
|
||||
|
||||
basePreferences.hardwareBitmapThreshold().changes()
|
||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||
.launchIn(scope)
|
||||
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
||||
@@ -209,17 +219,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
try {
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val chromiumElement = stackTrace.find {
|
||||
it.className.equals(
|
||||
"org.chromium.base.BuildInfo",
|
||||
ignoreCase = true,
|
||||
)
|
||||
}
|
||||
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
|
||||
return WebViewUtil.SPOOF_PACKAGE_NAME
|
||||
val isChromiumCall = stackTrace.any { trace ->
|
||||
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
|
||||
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
|
||||
}
|
||||
|
||||
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return super.getPackageName()
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@ import tachiyomi.domain.category.model.Category
|
||||
class BackupCategory(
|
||||
@ProtoNumber(1) var name: String,
|
||||
@ProtoNumber(2) var order: Long = 0,
|
||||
@ProtoNumber(3) var id: Long = 0,
|
||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||
@ProtoNumber(100) var flags: Long = 0,
|
||||
) {
|
||||
@@ -21,6 +22,7 @@ class BackupCategory(
|
||||
|
||||
val backupCategoryMapper = { category: Category ->
|
||||
BackupCategory(
|
||||
id = category.id,
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
|
@@ -25,6 +25,7 @@ data class BackupTracking(
|
||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||
// finishedReadingDate is called endReadTime in 1.x
|
||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||
@ProtoNumber(12) var private: Boolean = false,
|
||||
@ProtoNumber(100) var mediaId: Long = 0,
|
||||
) {
|
||||
|
||||
@@ -48,6 +49,7 @@ data class BackupTracking(
|
||||
startDate = this@BackupTracking.startedReadingDate,
|
||||
finishDate = this@BackupTracking.finishedReadingDate,
|
||||
remoteUrl = this@BackupTracking.trackingUrl,
|
||||
private = this@BackupTracking.private,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +68,7 @@ val backupTrackMapper = {
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
private: Boolean,
|
||||
->
|
||||
BackupTracking(
|
||||
syncId = syncId.toInt(),
|
||||
@@ -80,5 +83,6 @@ val backupTrackMapper = {
|
||||
startedReadingDate = startDate,
|
||||
finishedReadingDate = finishDate,
|
||||
trackingUrl = remoteUrl,
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ class BackupRestorer(
|
||||
restoreCategories(backup.backupCategories)
|
||||
}
|
||||
if (options.appSettings) {
|
||||
restoreAppPreferences(backup.backupPreferences)
|
||||
restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories })
|
||||
}
|
||||
if (options.sourceSettings) {
|
||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||
@@ -140,9 +140,15 @@ class BackupRestorer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
|
||||
private fun CoroutineScope.restoreAppPreferences(
|
||||
preferences: List<BackupPreference>,
|
||||
categories: List<BackupCategory>?,
|
||||
) = launch {
|
||||
ensureActive()
|
||||
preferenceRestorer.restoreApp(preferences)
|
||||
preferenceRestorer.restoreApp(
|
||||
preferences,
|
||||
categories,
|
||||
)
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(
|
||||
|
@@ -404,6 +404,7 @@ class MangaRestorer(
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
track.private,
|
||||
track.id,
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||
@@ -14,66 +16,122 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import tachiyomi.core.common.preference.AndroidPreferenceStore
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.plusAssign
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class PreferenceRestorer(
|
||||
private val context: Context,
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
) {
|
||||
|
||||
fun restoreApp(preferences: List<BackupPreference>) {
|
||||
restorePreferences(preferences, preferenceStore)
|
||||
suspend fun restoreApp(
|
||||
preferences: List<BackupPreference>,
|
||||
backupCategories: List<BackupCategory>?,
|
||||
) {
|
||||
restorePreferences(
|
||||
preferences,
|
||||
preferenceStore,
|
||||
backupCategories,
|
||||
)
|
||||
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
|
||||
fun restoreSource(preferences: List<BackupSourcePreferences>) {
|
||||
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
|
||||
preferences.forEach {
|
||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||
restorePreferences(it.prefs, sourcePrefs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
private suspend fun restorePreferences(
|
||||
toRestore: List<BackupPreference>,
|
||||
preferenceStore: PreferenceStore,
|
||||
backupCategories: List<BackupCategory>? = null,
|
||||
) {
|
||||
val allCategories = if (backupCategories != null) getCategories.await() else emptyList()
|
||||
val categoriesByName = allCategories.associateBy { it.name }
|
||||
val backupCategoriesById = backupCategories?.associateBy { it.id.toString() }.orEmpty()
|
||||
val prefs = preferenceStore.getAll()
|
||||
toRestore.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
preferenceStore.getInt(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
preferenceStore.getStringSet(key).set(value.value)
|
||||
try {
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) {
|
||||
backupCategoriesById[value.value.toString()]
|
||||
?.let { categoriesByName[it.name]?.id?.toInt() }
|
||||
} else {
|
||||
value.value
|
||||
}
|
||||
|
||||
newValue?.let { preferenceStore.getInt(key).set(it) }
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
val restored = restoreCategoriesPreference(
|
||||
key,
|
||||
value.value,
|
||||
preferenceStore,
|
||||
backupCategoriesById,
|
||||
categoriesByName,
|
||||
)
|
||||
if (!restored) preferenceStore.getStringSet(key).set(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PreferenceRestorer", "Failed to restore preference <$key>", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreCategoriesPreference(
|
||||
key: String,
|
||||
value: Set<String>,
|
||||
preferenceStore: PreferenceStore,
|
||||
backupCategoriesById: Map<String, BackupCategory>,
|
||||
categoriesByName: Map<String, Category>,
|
||||
): Boolean {
|
||||
val categoryPreferences = LibraryPreferences.categoryPreferenceKeys + DownloadPreferences.categoryPreferenceKeys
|
||||
if (key !in categoryPreferences) return false
|
||||
|
||||
val ids = value.mapNotNull {
|
||||
backupCategoriesById[it]?.name?.let { name ->
|
||||
categoriesByName[name]?.id?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.isNotEmpty()) {
|
||||
preferenceStore.getStringSet(key) += ids
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ import coil3.decode.ImageSource
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import coil3.request.bitmapConfig
|
||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
@@ -46,10 +45,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
|
||||
|
||||
check(bitmap != null) { "Failed to decode image" }
|
||||
|
||||
if (
|
||||
options.bitmapConfig == Bitmap.Config.HARDWARE &&
|
||||
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
|
||||
) {
|
||||
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
|
||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||
if (hwBitmap != null) {
|
||||
bitmap.recycle()
|
||||
|
@@ -27,6 +27,9 @@ interface Chapter : SChapter, Serializable {
|
||||
var version: Long
|
||||
}
|
||||
|
||||
val Chapter.isRecognizedNumber: Boolean
|
||||
get() = chapter_number >= 0f
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
if (id == null || manga_id == null) return null
|
||||
return DomainChapter(
|
||||
|
@@ -32,12 +32,15 @@ interface Track : Serializable {
|
||||
|
||||
var tracking_url: String
|
||||
|
||||
fun copyPersonalFrom(other: Track) {
|
||||
var private: Boolean
|
||||
|
||||
fun copyPersonalFrom(other: Track, copyRemotePrivate: Boolean = true) {
|
||||
last_chapter_read = other.last_chapter_read
|
||||
score = other.score
|
||||
status = other.status
|
||||
started_reading_date = other.started_reading_date
|
||||
finished_reading_date = other.finished_reading_date
|
||||
if (copyRemotePrivate) private = other.private
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@@ -29,4 +29,6 @@ class TrackImpl : Track {
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override var tracking_url: String = ""
|
||||
|
||||
override var private: Boolean = false
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@@ -454,7 +454,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
|
||||
|
||||
override fun deserialize(decoder: Decoder): UniFile? {
|
||||
return if (decoder.decodeNotNullMark()) {
|
||||
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
||||
UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri())
|
||||
} else {
|
||||
decoder.decodeNull()
|
||||
}
|
||||
|
@@ -0,0 +1,64 @@
|
||||
package eu.kanade.tachiyomi.data.export
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
object LibraryExporter {
|
||||
|
||||
data class ExportOptions(
|
||||
val includeTitle: Boolean,
|
||||
val includeAuthor: Boolean,
|
||||
val includeArtist: Boolean,
|
||||
)
|
||||
|
||||
suspend fun exportToCsv(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
favorites: List<Manga>,
|
||||
options: ExportOptions,
|
||||
onExportComplete: () -> Unit,
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
val csvData = generateCsvData(favorites, options)
|
||||
outputStream.write(csvData.toByteArray())
|
||||
}
|
||||
onExportComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private val escapeRequired = listOf("\r", "\n", "\"", ",")
|
||||
|
||||
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
|
||||
val columnSize = listOf(
|
||||
options.includeTitle,
|
||||
options.includeAuthor,
|
||||
options.includeArtist,
|
||||
)
|
||||
.count { it }
|
||||
|
||||
val rows = buildList(favorites.size) {
|
||||
favorites.forEach { manga ->
|
||||
buildList(columnSize) {
|
||||
if (options.includeTitle) add(manga.title)
|
||||
if (options.includeAuthor) add(manga.author)
|
||||
if (options.includeArtist) add(manga.artist)
|
||||
}
|
||||
.let(::add)
|
||||
}
|
||||
}
|
||||
return rows.joinToString("\r\n") { columns ->
|
||||
columns.joinToString(",") columns@{ column ->
|
||||
if (column.isNullOrBlank()) return@columns ""
|
||||
if (escapeRequired.any { column.contains(it) }) {
|
||||
column.replace("\"", "\"\"").let { "\"$it\"" }
|
||||
} else {
|
||||
column
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.library
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
@@ -92,10 +94,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (tags.contains(WORK_NAME_AUTO)) {
|
||||
val preferences = Injekt.get<LibraryPreferences>()
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.retry()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
val preferences = Injekt.get<LibraryPreferences>()
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
|
||||
return Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
// Find a running manual worker. If exists, try again later
|
||||
@@ -432,15 +436,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val interval = prefInterval ?: preferences.autoUpdateInterval().get()
|
||||
if (interval > 0) {
|
||||
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
},
|
||||
requiresCharging = DEVICE_CHARGING in restrictions,
|
||||
requiresBatteryNotLow = true,
|
||||
)
|
||||
val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||
interval.toLong(),
|
||||
|
@@ -71,6 +71,7 @@ object Notifications {
|
||||
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||
const val ID_APP_UPDATER = 1
|
||||
const val ID_APP_UPDATE_PROMPT = 2
|
||||
const val ID_APP_UPDATE_ERROR = 3
|
||||
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
const val ID_EXTENSION_INSTALLER = -402
|
||||
|
@@ -175,6 +175,7 @@ sealed class Image(
|
||||
}
|
||||
|
||||
sealed interface Location {
|
||||
@ConsistentCopyVisibility
|
||||
data class Pictures private constructor(val relativePath: String) : Location {
|
||||
companion object {
|
||||
fun create(relativePath: String = ""): Pictures {
|
||||
|
@@ -37,6 +37,8 @@ abstract class BaseTracker(
|
||||
// Application and remote support for reading dates
|
||||
override val supportsReadingDates: Boolean = false
|
||||
|
||||
override val supportsPrivateTracking: Boolean = false
|
||||
|
||||
// TODO: Store all scores as 10 point in the future maybe?
|
||||
override fun get10PointScore(track: DomainTrack): Double {
|
||||
return track.score
|
||||
@@ -120,6 +122,11 @@ abstract class BaseTracker(
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
override suspend fun setRemotePrivate(track: Track, private: Boolean) {
|
||||
track.private = private
|
||||
updateRemote(track)
|
||||
}
|
||||
|
||||
private suspend fun updateRemote(track: Track): Unit = withIOContext {
|
||||
try {
|
||||
update(track)
|
||||
|
@@ -22,6 +22,8 @@ interface Tracker {
|
||||
// Application and remote support for reading dates
|
||||
val supportsReadingDates: Boolean
|
||||
|
||||
val supportsPrivateTracking: Boolean
|
||||
|
||||
@ColorInt
|
||||
fun getLogoColor(): Int
|
||||
|
||||
@@ -82,4 +84,6 @@ interface Tracker {
|
||||
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
|
||||
|
||||
suspend fun setRemotePrivate(track: Track, private: Boolean)
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -43,6 +42,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override val supportsPrivateTracking: Boolean = true
|
||||
|
||||
private val scorePreference = trackPreferences.anilistScoreType()
|
||||
|
||||
init {
|
||||
@@ -183,7 +184,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||
track.library_id = remoteTrack.library_id
|
||||
|
||||
if (track.status != COMPLETED) {
|
||||
|
@@ -42,8 +42,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
||||
| id
|
||||
| status
|
||||
|}
|
||||
@@ -56,6 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("mangaId", track.remote_id)
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
put("status", track.toApiStatus())
|
||||
put("private", track.private)
|
||||
}
|
||||
}
|
||||
with(json) {
|
||||
@@ -79,11 +80,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|mutation UpdateManga(
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|
||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||
|) {
|
||||
|SaveMediaListEntry(
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|
||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||
|) {
|
||||
|id
|
||||
@@ -102,6 +103,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("score", track.score.toInt())
|
||||
put("startedAt", createDate(track.started_reading_date))
|
||||
put("completedAt", createDate(track.finished_reading_date))
|
||||
put("private", track.private)
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
@@ -138,6 +140,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
|id
|
||||
|staff {
|
||||
|edges {
|
||||
|role
|
||||
|id
|
||||
|node {
|
||||
|name {
|
||||
|full
|
||||
|userPreferred
|
||||
|native
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|title {
|
||||
|userPreferred
|
||||
|}
|
||||
@@ -190,6 +205,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|status
|
||||
|scoreRaw: score(format: POINT_100)
|
||||
|progress
|
||||
|private
|
||||
|startedAt {
|
||||
|year
|
||||
|month
|
||||
@@ -217,6 +233,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|month
|
||||
|day
|
||||
|}
|
||||
|staff {
|
||||
|edges {
|
||||
|role
|
||||
|id
|
||||
|node {
|
||||
|name {
|
||||
|full
|
||||
|userPreferred
|
||||
|native
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|}
|
||||
|
@@ -19,6 +19,7 @@ data class ALManga(
|
||||
val startDateFuzzy: Long,
|
||||
val totalChapters: Long,
|
||||
val averageScore: Int,
|
||||
val staff: ALStaff,
|
||||
) {
|
||||
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||
remote_id = remoteId
|
||||
@@ -38,6 +39,11 @@ data class ALManga(
|
||||
""
|
||||
}
|
||||
}
|
||||
staff.edges.forEach {
|
||||
val name = it.node.name() ?: return@forEach
|
||||
if ("Story" in it.role) authors += name
|
||||
if ("Art" in it.role) artists += name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +55,7 @@ data class ALUserManga(
|
||||
val startDateFuzzy: Long,
|
||||
val completedDateFuzzy: Long,
|
||||
val manga: ALManga,
|
||||
val private: Boolean,
|
||||
) {
|
||||
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
||||
remote_id = manga.remoteId
|
||||
@@ -60,6 +67,7 @@ data class ALUserManga(
|
||||
last_chapter_read = chaptersRead.toDouble()
|
||||
library_id = libraryId
|
||||
total_chapters = manga.totalChapters
|
||||
private = this@ALUserManga.private
|
||||
}
|
||||
|
||||
private fun toTrackStatus() = when (listStatus) {
|
||||
|
@@ -13,6 +13,7 @@ data class ALSearchItem(
|
||||
val startDate: ALFuzzyDate,
|
||||
val chapters: Long?,
|
||||
val averageScore: Int?,
|
||||
val staff: ALStaff,
|
||||
) {
|
||||
fun toALManga(): ALManga = ALManga(
|
||||
remoteId = id,
|
||||
@@ -24,6 +25,7 @@ data class ALSearchItem(
|
||||
startDateFuzzy = startDate.toEpochMilli(),
|
||||
totalChapters = chapters ?: 0,
|
||||
averageScore = averageScore ?: -1,
|
||||
staff = staff,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,3 +38,31 @@ data class ALItemTitle(
|
||||
data class ItemCover(
|
||||
val large: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaff(
|
||||
val edges: List<ALEdge>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALEdge(
|
||||
val role: String,
|
||||
val id: Int,
|
||||
val node: ALStaffNode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffNode(
|
||||
val name: ALStaffName,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALStaffName(
|
||||
val userPreferred: String?,
|
||||
val native: String?,
|
||||
val full: String?,
|
||||
) {
|
||||
operator fun invoke(): String? {
|
||||
return userPreferred ?: full ?: native
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ data class ALUserListItem(
|
||||
val startedAt: ALFuzzyDate,
|
||||
val completedAt: ALFuzzyDate,
|
||||
val media: ALSearchItem,
|
||||
val private: Boolean,
|
||||
) {
|
||||
fun toALUserManga(): ALUserManga {
|
||||
return ALUserManga(
|
||||
@@ -38,6 +39,7 @@ data class ALUserListItem(
|
||||
startDateFuzzy = startedAt.toEpochMilli(),
|
||||
completedDateFuzzy = completedAt.toEpochMilli(),
|
||||
manga = media.toALManga(),
|
||||
private = private,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -23,6 +22,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
|
||||
private val api by lazy { BangumiApi(id, client, interceptor) }
|
||||
|
||||
override val supportsPrivateTracking: Boolean = true
|
||||
|
||||
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
||||
|
||||
override fun displayScore(track: DomainTrack): String {
|
||||
@@ -48,26 +49,23 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
return if (remoteTrack != null && statusTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
|
||||
val statusTrack = api.statusLibManga(track, getUsername())
|
||||
return if (statusTrack != null) {
|
||||
track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
|
||||
track.library_id = statusTrack.library_id
|
||||
track.score = statusTrack.score
|
||||
track.last_chapter_read = statusTrack.last_chapter_read
|
||||
track.total_chapters = statusTrack.total_chapters
|
||||
if (track.status != COMPLETED) {
|
||||
track.status = if (hasReadChapters) READING else statusTrack.status
|
||||
}
|
||||
|
||||
track.score = statusTrack.score
|
||||
track.last_chapter_read = statusTrack.last_chapter_read
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
refresh(track)
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||
track.score = 0.0
|
||||
add(track)
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +74,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
|
||||
val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga")
|
||||
track.copyPersonalFrom(remoteStatusTrack)
|
||||
api.findLibManga(track)?.let { remoteTrack ->
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
@@ -113,8 +108,12 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
try {
|
||||
val oauth = api.accessToken(code)
|
||||
interceptor.newAuth(oauth)
|
||||
saveCredentials(oauth.userId.toString(), oauth.accessToken)
|
||||
} catch (e: Throwable) {
|
||||
// Users can set a 'username' (not nickname) once which effectively
|
||||
// replaces the stringified ID in certain queries.
|
||||
// If no username is set, the API returns the user ID as a strings
|
||||
var username = api.getUsername()
|
||||
saveCredentials(username, oauth.accessToken)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
@@ -126,7 +125,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
fun restoreToken(): BGMOAuth? {
|
||||
return try {
|
||||
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -138,11 +137,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val READING = 3L
|
||||
const val PLAN_TO_READ = 1L
|
||||
const val COMPLETED = 2L
|
||||
const val READING = 3L
|
||||
const val ON_HOLD = 4L
|
||||
const val DROPPED = 5L
|
||||
const val PLAN_TO_READ = 1L
|
||||
|
||||
private val SCORE_LIST = IntRange(0, 10)
|
||||
.map(Int::toString)
|
||||
|
@@ -5,22 +5,28 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers.Companion.headersOf
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class BangumiApi(
|
||||
private val trackId: Long,
|
||||
@@ -34,11 +40,17 @@ class BangumiApi(
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val body = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toApiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
|
||||
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||
val body = buildJsonObject {
|
||||
put("type", track.toApiStatus())
|
||||
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||
put("ep_status", track.last_chapter_read.toInt())
|
||||
put("private", track.private)
|
||||
}
|
||||
.toString()
|
||||
.toRequestBody()
|
||||
// Returns with 202 Accepted on success with no body
|
||||
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
||||
.awaitSuccess()
|
||||
track
|
||||
}
|
||||
@@ -46,106 +58,110 @@ class BangumiApi(
|
||||
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toApiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
|
||||
.awaitSuccess()
|
||||
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||
val body = buildJsonObject {
|
||||
put("type", track.toApiStatus())
|
||||
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||
put("ep_status", track.last_chapter_read.toInt())
|
||||
put("private", track.private)
|
||||
}
|
||||
.toString()
|
||||
.toRequestBody()
|
||||
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toInt().toString())
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.patch(body)
|
||||
.headers(headersOf("Content-Type", APP_JSON))
|
||||
.build()
|
||||
authClient.newCall(
|
||||
POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body),
|
||||
).awaitSuccess()
|
||||
// Returns with 204 No Content
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
// This API is marked as experimental in the documentation
|
||||
// but that has been the case since 2022 with few significant
|
||||
// changes to the schema for this endpoint since
|
||||
// "实验性 API, 本 schema 和实际的 API 行为都可能随时发生改动"
|
||||
return withIOContext {
|
||||
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val url = "$API_URL/v0/search/subjects?limit=20"
|
||||
val body = buildJsonObject {
|
||||
put("keyword", search)
|
||||
put("sort", "match")
|
||||
putJsonObject("filter") {
|
||||
putJsonArray("type") {
|
||||
add(1) // "Book" (书籍) type
|
||||
}
|
||||
}
|
||||
}
|
||||
.toString()
|
||||
.toRequestBody()
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchResult>()
|
||||
.let { result ->
|
||||
if (result.code == 404) emptyList<TrackSearch>()
|
||||
|
||||
result.list
|
||||
?.filter { it.type == 1 }
|
||||
?.map { it.toTrackSearch(trackId) }
|
||||
.orEmpty()
|
||||
}
|
||||
.data
|
||||
.map { it.toTrackSearch(trackId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
suspend fun statusLibManga(track: Track, username: String): Track? {
|
||||
return withIOContext {
|
||||
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
|
||||
with(json) {
|
||||
authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMSearchItem>()
|
||||
.toTrackSearch(trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun statusLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
val urlUserRead = "$API_URL/collection/${track.remote_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
// TODO: get user readed chapter here
|
||||
with(json) {
|
||||
authClient.newCall(requestUserRead)
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMCollectionResponse>()
|
||||
.let {
|
||||
if (it.code == 400) return@let null
|
||||
|
||||
track.status = it.status?.id!!
|
||||
track.last_chapter_read = it.epStatus!!.toDouble()
|
||||
track.score = it.rating!!
|
||||
track
|
||||
try {
|
||||
authClient.newCall(GET(url, cache = CacheControl.FORCE_NETWORK))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMCollectionResponse>()
|
||||
.let {
|
||||
track.status = it.getStatus()
|
||||
track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
|
||||
track.score = it.rate?.toDouble() ?: 0.0
|
||||
track.total_chapters = it.subject?.eps?.toLong() ?: 0L
|
||||
track
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
if (e.code == 404) { // "subject is not collected by user"
|
||||
null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): BGMOAuth {
|
||||
return withIOContext {
|
||||
val body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", REDIRECT_URL)
|
||||
.build()
|
||||
with(json) {
|
||||
client.newCall(accessTokenRequest(code))
|
||||
client.newCall(POST(OAUTH_URL, body = body))
|
||||
.awaitSuccess()
|
||||
.parseAs()
|
||||
.parseAs<BGMOAuth>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
OAUTH_URL,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", CLIENT_ID)
|
||||
.add("client_secret", CLIENT_SECRET)
|
||||
.add("code", code)
|
||||
.add("redirect_uri", REDIRECT_URL)
|
||||
.build(),
|
||||
)
|
||||
suspend fun getUsername(): String {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$API_URL/v0/me"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMUser>()
|
||||
.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CLIENT_ID = "bgm291665acbd06a4c28"
|
||||
@@ -157,6 +173,8 @@ class BangumiApi(
|
||||
|
||||
private const val REDIRECT_URL = "mihon://bangumi-auth"
|
||||
|
||||
private const val APP_JSON = "application/json"
|
||||
|
||||
fun authUrl(): Uri =
|
||||
LOGIN_URL.toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", CLIENT_ID)
|
||||
|
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -21,12 +20,13 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
|
||||
var currAuth: BGMOAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
|
||||
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(json.decodeFromString<BGMOAuth>(response.body.string()))
|
||||
currAuth = json.decodeFromString<BGMOAuth>(response.body.string())
|
||||
newAuth(currAuth)
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
@@ -38,14 +38,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
||||
"antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
|
||||
)
|
||||
.apply {
|
||||
if (originalRequest.method == "GET") {
|
||||
val newUrl = originalRequest.url.newBuilder()
|
||||
.addQueryParameter("access_token", currAuth.accessToken)
|
||||
.build()
|
||||
url(newUrl)
|
||||
} else {
|
||||
post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
|
||||
}
|
||||
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
||||
}
|
||||
.build()
|
||||
.let(chain::proceed)
|
||||
@@ -67,13 +60,4 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
||||
|
||||
bangumi.saveToken(oauth)
|
||||
}
|
||||
|
||||
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
|
||||
val newFormBody = FormBody.Builder()
|
||||
for (i in 0..<oidFormBody.size) {
|
||||
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
|
||||
}
|
||||
newFormBody.add("access_token", token)
|
||||
return newFormBody.build()
|
||||
}
|
||||
}
|
||||
|
@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toApiStatus() = when (status) {
|
||||
Bangumi.READING -> "do"
|
||||
Bangumi.COMPLETED -> "collect"
|
||||
Bangumi.ON_HOLD -> "on_hold"
|
||||
Bangumi.DROPPED -> "dropped"
|
||||
Bangumi.PLAN_TO_READ -> "wish"
|
||||
Bangumi.PLAN_TO_READ -> 1
|
||||
Bangumi.COMPLETED -> 2
|
||||
Bangumi.READING -> 3
|
||||
Bangumi.ON_HOLD -> 4
|
||||
Bangumi.DROPPED -> 5
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
@@ -1,28 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMCollectionResponse(
|
||||
val code: Int?,
|
||||
val `private`: Int? = 0,
|
||||
val comment: String? = "",
|
||||
val rate: Int?,
|
||||
val type: Int?,
|
||||
@SerialName("ep_status")
|
||||
val epStatus: Int? = 0,
|
||||
@SerialName("lasttouch")
|
||||
val lastTouch: Int? = 0,
|
||||
val rating: Double? = 0.0,
|
||||
val status: Status? = Status(),
|
||||
val tag: List<String?>? = emptyList(),
|
||||
val user: User? = User(),
|
||||
@SerialName("vol_status")
|
||||
val volStatus: Int? = 0,
|
||||
)
|
||||
val private: Boolean = false,
|
||||
val subject: BGMSlimSubject? = null,
|
||||
) {
|
||||
fun getStatus(): Long = when (type) {
|
||||
1 -> Bangumi.PLAN_TO_READ
|
||||
2 -> Bangumi.COMPLETED
|
||||
3 -> Bangumi.READING
|
||||
4 -> Bangumi.ON_HOLD
|
||||
5 -> Bangumi.DROPPED
|
||||
else -> throw NotImplementedError("Unknown status: $type")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Status(
|
||||
val id: Long? = 0,
|
||||
val name: String? = "",
|
||||
val type: String? = "",
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMSlimSubject(
|
||||
val volumes: Int?,
|
||||
val eps: Int?,
|
||||
)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -10,6 +11,7 @@ data class BGMOAuth(
|
||||
@SerialName("token_type")
|
||||
val tokenType: String,
|
||||
@SerialName("created_at")
|
||||
@EncodeDefault
|
||||
val createdAt: Long = System.currentTimeMillis() / 1000,
|
||||
@SerialName("expires_in")
|
||||
val expiresIn: Long,
|
||||
|
@@ -6,40 +6,50 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class BGMSearchResult(
|
||||
val list: List<BGMSearchItem>?,
|
||||
val code: Int?,
|
||||
val total: Int,
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val data: List<BGMSubject> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BGMSearchItem(
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMSubject(
|
||||
val id: Long,
|
||||
@SerialName("name_cn")
|
||||
val nameCn: String,
|
||||
val name: String,
|
||||
val type: Int,
|
||||
val images: BGMSearchItemCovers?,
|
||||
@SerialName("eps_count")
|
||||
val epsCount: Long?,
|
||||
val rating: BGMSearchItemRating?,
|
||||
val url: String,
|
||||
val summary: String?,
|
||||
val date: String?, // YYYY-MM-DD
|
||||
val images: BGMSubjectImages?,
|
||||
val volumes: Long = 0,
|
||||
val eps: Long = 0,
|
||||
val rating: BGMSubjectRating?,
|
||||
) {
|
||||
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
||||
remote_id = this@BGMSearchItem.id
|
||||
title = nameCn
|
||||
cover_url = images?.common ?: ""
|
||||
summary = this@BGMSearchItem.name
|
||||
remote_id = this@BGMSubject.id
|
||||
title = nameCn.ifBlank { name }
|
||||
cover_url = images?.common.orEmpty()
|
||||
summary = if (nameCn.isNotBlank()) {
|
||||
"作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
|
||||
} else {
|
||||
this@BGMSubject.summary?.trim().orEmpty()
|
||||
}
|
||||
score = rating?.score ?: -1.0
|
||||
tracking_url = url
|
||||
total_chapters = epsCount ?: 0
|
||||
tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
|
||||
total_chapters = eps
|
||||
start_date = date ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BGMSearchItemCovers(
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMSubjectImages(
|
||||
val common: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BGMSearchItemRating(
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMSubjectRating(
|
||||
val score: Double?,
|
||||
)
|
||||
|
@@ -1,23 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.track.bangumi.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Avatar(
|
||||
val large: String? = "",
|
||||
val medium: String? = "",
|
||||
val small: String? = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val avatar: Avatar? = Avatar(),
|
||||
val id: Int? = 0,
|
||||
val nickname: String? = "",
|
||||
val sign: String? = "",
|
||||
val url: String? = "",
|
||||
@SerialName("usergroup")
|
||||
val userGroup: Int? = 0,
|
||||
val username: String? = "",
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMUser(
|
||||
val username: String,
|
||||
)
|
||||
|
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -29,6 +28,8 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override val supportsPrivateTracking: Boolean = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||
@@ -101,7 +102,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUserId())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||
track.remote_id = remoteTrack.remote_id
|
||||
|
||||
if (track.status != COMPLETED) {
|
||||
@@ -150,7 +151,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
fun restoreToken(): KitsuOAuth? {
|
||||
return try {
|
||||
json.decodeFromString<KitsuOAuth>(trackPreferences.trackToken(this).get())
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@@ -46,6 +46,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
putJsonObject("attributes") {
|
||||
put("status", track.toApiStatus())
|
||||
put("progress", track.last_chapter_read.toInt())
|
||||
put("private", track.private)
|
||||
}
|
||||
putJsonObject("relationships") {
|
||||
putJsonObject("user") {
|
||||
@@ -94,6 +95,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
||||
put("ratingTwenty", track.toApiScore())
|
||||
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||
put("private", track.private)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -42,6 +42,7 @@ data class KitsuListSearchResult(
|
||||
}
|
||||
score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0
|
||||
last_chapter_read = userDataAttrs.progress.toDouble()
|
||||
private = userDataAttrs.private
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +60,7 @@ data class KitsuListSearchItemDataAttributes(
|
||||
val finishedAt: String?,
|
||||
val ratingTwenty: Int?,
|
||||
val progress: Int,
|
||||
val private: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@@ -30,8 +30,14 @@ class TrackSearch : Track {
|
||||
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override var private: Boolean = false
|
||||
|
||||
override lateinit var tracking_url: String
|
||||
|
||||
var authors: List<String> = emptyList()
|
||||
|
||||
var artists: List<String> = emptyList()
|
||||
|
||||
var cover_url: String = ""
|
||||
|
||||
var summary: String = ""
|
||||
|
@@ -111,7 +111,7 @@ class MyAnimeListApi(
|
||||
summary = it.synopsis
|
||||
total_chapters = it.numChapters
|
||||
score = it.mean
|
||||
cover_url = it.covers.large
|
||||
cover_url = it.covers?.large.orEmpty()
|
||||
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||
publishing_status = it.status.replace("_", " ")
|
||||
publishing_type = it.mediaType.replace("_", " ")
|
||||
|
@@ -12,7 +12,7 @@ data class MALManga(
|
||||
val numChapters: Long,
|
||||
val mean: Double = -1.0,
|
||||
@SerialName("main_picture")
|
||||
val covers: MALMangaCovers,
|
||||
val covers: MALMangaCovers?,
|
||||
val status: String,
|
||||
@SerialName("media_type")
|
||||
val mediaType: String,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist.dto
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -14,10 +15,11 @@ data class MALOAuth(
|
||||
@SerialName("expires_in")
|
||||
val expiresIn: Long,
|
||||
@SerialName("created_at")
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
@EncodeDefault
|
||||
val createdAt: Long = System.currentTimeMillis() / 1000,
|
||||
) {
|
||||
// Assumes expired a minute earlier
|
||||
private val adjustedExpiresIn: Long = (expiresIn - 60) * 1000
|
||||
private val adjustedExpiresIn: Long = (expiresIn - 60)
|
||||
|
||||
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis()
|
||||
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() / 1000
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user