mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 21:52:25 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81e0754c0d | ||
|
|
e287f08789 | ||
|
|
a73271aefa | ||
|
|
f7a1ecd251 | ||
|
|
f80c91081f | ||
|
|
55be95dd5d | ||
|
|
d7f3ceef5c | ||
|
|
1d8a2b05dd | ||
|
|
6d69903a56 | ||
|
|
0648e2eaaf | ||
|
|
7d4e1476ba | ||
|
+9 |
46a2471abb | ||
|
|
f63dc799f0 | ||
|
|
c114028a33 | ||
|
|
4296d056b5 | ||
|
|
7aaa891aef | ||
|
|
2ccb0ca163 | ||
|
|
44cd9a55d1 | ||
|
|
2a92f0b46a | ||
|
|
f37e81991e | ||
|
|
d15a05602a | ||
|
|
fbf5ae82f7 | ||
|
|
fdb223d657 | ||
|
|
54ae69eaa2 | ||
|
|
45b1e781eb | ||
|
|
069e5c91ac | ||
|
|
4ac9f3474b | ||
|
|
9c3cc1ca5f | ||
|
|
47d7c0f2ea | ||
|
|
0b5e9bb815 | ||
|
|
d72ae365cb | ||
|
|
e7ec672c8b | ||
|
|
628cb58862 | ||
|
|
e454cf8e9d | ||
|
|
baa0fb7ecb | ||
|
|
2d2cf0879f | ||
|
|
6981185f4b | ||
|
|
47aa9190d4 | ||
|
|
98705910e7 | ||
|
|
5ce7d00eb8 | ||
|
|
03229a3807 | ||
|
|
493140c7ff | ||
|
|
98bb731b4d | ||
|
|
c3b99aea07 | ||
|
|
ef1c529672 | ||
|
|
633eea4ced | ||
|
|
31a4d6ba39 | ||
|
|
e9b7c760a1 | ||
|
|
ef1e7ba907 | ||
|
|
526d79a975 | ||
|
|
b9ac9a0422 | ||
|
|
d171768cff | ||
|
|
cb2617dee0 | ||
|
|
b4635c41a8 | ||
|
|
217b20123c | ||
|
|
b2ca6bc5f3 | ||
|
|
a8d3e39779 | ||
|
|
3d3bd49d1c | ||
|
|
4a66b8b5df | ||
|
|
52f827c6ff | ||
|
|
aa434285b2 | ||
|
|
91e967128d | ||
|
|
abef1cb8c5 | ||
|
|
1d8e5d7eb1 | ||
|
|
75f506836c | ||
|
|
44780e74b5 | ||
|
|
8407439b8a | ||
|
|
ab58a9952e | ||
|
|
0ff654205f | ||
|
|
1d8789d800 | ||
|
|
a313c747f3 | ||
|
|
66b390c62a | ||
|
|
26abad0be0 | ||
|
|
68f293be45 | ||
|
|
41f4579c74 | ||
|
|
0787678c12 | ||
|
|
d0c79399b9 | ||
|
|
17aed02e56 | ||
|
|
88a9d44bb7 | ||
|
|
aa46039e6c | ||
|
|
3b078331a0 | ||
|
|
b8e5f22c01 | ||
|
|
c3bf7a78c0 | ||
|
|
94b3b5eaa2 | ||
|
|
27284a40ab | ||
|
|
76cc04c8f4 | ||
|
|
77580cd55f | ||
|
|
db095c87aa | ||
|
|
f3c6789dfb | ||
|
|
0772f72021 | ||
|
|
1623c188df | ||
|
|
dfed97edc8 | ||
|
|
f6632940fa | ||
|
|
ca2e8cf4de | ||
|
|
000e2d091b | ||
|
|
40f782e10c | ||
|
|
f306e65b25 | ||
|
|
d0f373df22 | ||
|
|
3fd76e4f93 | ||
|
|
d8c3440d37 | ||
|
|
a82ccea6f5 |
@@ -30,7 +30,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.20.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.20.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
label: Mihon version
|
||||
description: You can find your Mihon version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.20.0"
|
||||
Example: "0.20.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -95,7 +95,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.20.0](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.20.3](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
required: true
|
||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||
required: true
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
paths:
|
||||
- '**'
|
||||
- '!**.md'
|
||||
- '!fastlane/**'
|
||||
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
||||
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
||||
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
||||
@@ -26,20 +27,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
@@ -32,16 +32,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
@@ -80,16 +80,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
|
||||
with:
|
||||
cache-disabled: true
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
mihon-foss
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
with:
|
||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||
|
||||
+72
-1
@@ -12,6 +12,74 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.20.3] - 2026-08-04
|
||||
### Added
|
||||
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
||||
- Add Category filtering for the Upcoming calendar ([@MajorTanya](https://github.com/MajorTanya)) ([#3607](https://github.com/mihonapp/mihon/pull/3607))
|
||||
|
||||
### Changed
|
||||
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Improved
|
||||
- Spoof `Sec-CH-UA` client hints in WebView to match the user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
|
||||
### Fixed
|
||||
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676))
|
||||
- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||
- Fixed reader loading indefinitely in some scenarios ([@AntsyLich](https://github.com/AntsyLich)) ([#3686](https://github.com/mihonapp/mihon/pull/3686))
|
||||
- Fix MyAnimeList error for list entries with partial start/finish reading dates by setting those dates to either Jan 1st or 1st of the month ([@MajorTanya](https://github.com/MajorTanya)) ([#3573](https://github.com/mihonapp/mihon/pull/3573))
|
||||
|
||||
## [v0.20.2] - 2026-08-01
|
||||
### Added
|
||||
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||
- Fix HTTP Error 416 not being handled properly ([@AntsyLich](https://github.com/AntsyLich)) ([#3563](https://github.com/mihonapp/mihon/pull/3563))
|
||||
- Fix incorrect progress when resuming downloads ([@xMohnad](https://github.com/xMohnad)) ([#3616](https://github.com/mihonapp/mihon/pull/3616))
|
||||
- Add Tokyo Night Theme ([@xMohnad](https://github.com/xMohnad)) ([#3502](https://github.com/mihonapp/mihon/pull/3502))
|
||||
|
||||
### Changed
|
||||
- Detect Shizuku with permission check ([@Small-Ku](https://github.com/Small-Ku)) ([#3565](https://github.com/mihonapp/mihon/pull/3565))
|
||||
- Check for app and extension update on every cold start ([@AntsyLich](https://github.com/AntsyLich)) ([#3658](https://github.com/mihonapp/mihon/pull/3658))
|
||||
|
||||
### Improved
|
||||
- Library search ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3554](https://github.com/mihonapp/mihon/pull/3554))
|
||||
- Support logical operators (`&&`, `||`, `-`), implicit `AND` separators, and nested expressions with `()`
|
||||
- Add comparison operators (`>`, `>=`, `<`, `<=`, `=`) for `id`, `added`, `fetchinterval` / `fi`, `nextupdate` / `nu`, `unread`, `read`, and `total` (Uses `yyyy-MM-dd` dates for `added` and `nextupdate`)
|
||||
- Support field-specific prefixes (`title`, `author`, `artist`, `description` / `desc`, `genre` / `tag`, `source` / `src`, `source_id` / `src_id`, `notes` / `note`, `language` / `lang`) and empty field matching (`""`, `''`)
|
||||
- Always use Private installer to update privately installed extensions ([@MajorTanya](https://github.com/MajorTanya)) ([#3614](https://github.com/mihonapp/mihon/pull/3614))
|
||||
- App update prompt flow experience ([@AntsyLich](https://github.com/AntsyLich)) ([#3669](https://github.com/mihonapp/mihon/pull/3669))
|
||||
|
||||
### Fixed
|
||||
- Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider ([@AntsyLich](https://github.com/AntsyLich)) ([#3567](https://github.com/mihonapp/mihon/pull/3567))
|
||||
- Fixed MangaBaka User Agent string ([@MajorTanya](https://github.com/MajorTanya)) ([#3578](https://github.com/mihonapp/mihon/pull/3578))
|
||||
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3630](https://github.com/mihonapp/mihon/pull/3630))
|
||||
- Fixed backup restore dropping library entries when the backup contains duplicate chapters ([@na-ji](https://github.com/na-ji)) ([#3667](https://github.com/mihonapp/mihon/pull/3667))
|
||||
|
||||
## [v0.20.1] - 2026-07-09
|
||||
### Added
|
||||
- [Hikka](https://hikka.io/) tracker support ([@Lorg0n](https://github.com/Lorg0n)) ([#1386](https://github.com/mihonapp/mihon/pull/1386))
|
||||
- Fix Hikka not defaulting to "Plan to Read" for unread titles ([@MajorTanya](https://github.com/MajorTanya)) ([#3534](https://github.com/mihonapp/mihon/pull/3534))
|
||||
- Fix Hikka search throwing error due to unclosed response ([@MajorTanya](https://github.com/MajorTanya)) ([#3548](https://github.com/mihonapp/mihon/pull/3548))
|
||||
- Add support for [MangaBaka](https://mangabaka.org) tracker ([@MajorTanya](https://github.com/MajorTanya)) ([#3047](https://github.com/mihonapp/mihon/pull/3047))
|
||||
- Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499))
|
||||
- Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096))
|
||||
- Add setting to control vertical chapter navigator height ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||
- Add option to set which readers use vertical chapter navigator ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||
|
||||
### Changed
|
||||
- Don't use GMS to detect if GMS is available ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3525](https://github.com/mihonapp/mihon/pull/3525))
|
||||
|
||||
### Improved
|
||||
- Show usernames in Tracking settings ([@MajorTanya](https://github.com/MajorTanya)) ([#3533](https://github.com/mihonapp/mihon/pull/3533))
|
||||
|
||||
### Fixed
|
||||
- Fix Shikimori tracking not working ([@MajorTanya](https://github.com/MajorTanya)) ([#3497](https://github.com/mihonapp/mihon/pull/3497))
|
||||
- Fix crash trying to select text in notes screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3516](https://github.com/mihonapp/mihon/pull/3516))
|
||||
- Fix crash when putting app in background ([@AntsyLich](https://github.com/AntsyLich)) ([#3523](https://github.com/mihonapp/mihon/pull/3523))
|
||||
- Fix support for non-system SAF providers ([@AntsyLich](https://github.com/AntsyLich)) ([#3530](https://github.com/mihonapp/mihon/pull/3530))
|
||||
- Fix chapter's `memo` field not being updated for existing chapters ([@cuong-tran](https://github.com/cuong-tran)) ([#3538](https://github.com/mihonapp/mihon/pull/3538))
|
||||
- Fix app not reading `tachiyomix.extensionLib` extension metadata ([@AntsyLich](https://github.com/AntsyLich)) ([#3545](https://github.com/mihonapp/mihon/pull/3545), [#3559](https://github.com/mihonapp/mihon/pull/3559))
|
||||
- Fix reader navigator slider steps not updating after changing chapter ([@AntsyLich](https://github.com/AntsyLich)) ([#3549](https://github.com/mihonapp/mihon/pull/3549))
|
||||
|
||||
## [v0.20.0] - 2026-06-27
|
||||
### Added
|
||||
- Add support for `tachiyomix` 1.6 extensions ([@AntsyLich](https://github.com/AntsyLich))
|
||||
@@ -550,7 +618,10 @@ Same as v0.19.6
|
||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
||||
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
||||
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.0...main
|
||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.3...main
|
||||
[v0.20.3]: https://github.com/mihonapp/mihon/compare/v0.20.2...v0.20.3
|
||||
[v0.20.2]: https://github.com/mihonapp/mihon/compare/v0.20.1...v0.20.2
|
||||
[v0.20.1]: https://github.com/mihonapp/mihon/compare/v0.20.0...v0.20.1
|
||||
[v0.20.0]: https://github.com/mihonapp/mihon/compare/v0.19.9...v0.20.0
|
||||
[v0.19.9]: https://github.com/mihonapp/mihon/compare/v0.19.8...v0.19.9
|
||||
[v0.19.8]: https://github.com/mihonapp/mihon/compare/v0.19.7...v0.19.8
|
||||
|
||||
@@ -29,7 +29,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
|
||||
* Local reading of content.
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support.
|
||||
* Tracker support: [MangaBaka](https://mangabaka.org), [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/), and [Hikka](https://hikka.io/) support.
|
||||
* Categories to organize your library.
|
||||
* Light and dark themes.
|
||||
* Schedule updating your library for new chapters.
|
||||
|
||||
@@ -32,8 +32,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 24
|
||||
versionName = "0.20.0"
|
||||
versionCode = 28
|
||||
versionName = "0.20.3"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
|
||||
@@ -78,8 +78,8 @@ android {
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
val release = getByName("release") {
|
||||
isMinifyEnabled = Config.enableCodeShrink
|
||||
isShrinkResources = Config.enableCodeShrink
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
|
||||
signingConfig = debug.signingConfig
|
||||
|
||||
@@ -209,6 +209,7 @@ dependencies {
|
||||
implementation(projects.i18n)
|
||||
implementation(projects.core.archive)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.viewmodel)
|
||||
implementation(projects.coreMetadata)
|
||||
implementation(projects.sourceApi)
|
||||
implementation(projects.sourceLocal)
|
||||
@@ -242,6 +243,8 @@ dependencies {
|
||||
|
||||
implementation(libs.sqldelight.async)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.appCompat)
|
||||
|
||||
Vendored
+15
@@ -16,6 +16,7 @@
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
-keep,allowoptimization class com.squareup.zstd.** { public protected *; }
|
||||
|
||||
# From extensions-lib
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
|
||||
@@ -25,6 +26,11 @@
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
@@ -80,3 +86,12 @@
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
||||
# KotlinX Datetime
|
||||
-keep,allowoptimization class kotlinx.datetime.** { public protected *; }
|
||||
|
||||
# Methods called by Shizuku only
|
||||
-keepclassmembers class mihon.app.shizuku.ShellInterface {
|
||||
public <init>();
|
||||
public void destroy();
|
||||
}
|
||||
|
||||
@@ -193,8 +193,10 @@
|
||||
|
||||
<data android:host="anilist-auth" />
|
||||
<data android:host="bangumi-auth" />
|
||||
<data android:host="mangabaka-auth" />
|
||||
<data android:host="myanimelist-auth" />
|
||||
<data android:host="shikimori-auth" />
|
||||
<data android:host="hikka-auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package mihon.app.shizuku;
|
||||
|
||||
interface IShellInterface {
|
||||
void install(in AssetFileDescriptor apk) = 1;
|
||||
void install(in AssetFileDescriptor apk, in IntentSender intentSender) = 1;
|
||||
|
||||
void destroy() = 16777114;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -143,7 +143,7 @@ class DomainModule : InjektModule {
|
||||
}
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
addFactory { GetApplicationRelease(get(), get()) }
|
||||
addFactory { GetApplicationRelease(get()) }
|
||||
|
||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||
addFactory { TrackChapter(get(), get(), get(), get()) }
|
||||
|
||||
@@ -10,6 +10,9 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.data.chapter.ChapterSanitizer
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
@@ -23,8 +26,8 @@ import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
import kotlin.time.Clock
|
||||
|
||||
class SyncChaptersWithSource(
|
||||
private val downloadManager: DownloadManager,
|
||||
@@ -57,8 +60,9 @@ class SyncChaptersWithSource(
|
||||
throw NoChaptersException()
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val now = Clock.System.now().toLocalDateTime(timeZone)
|
||||
val nowMillis = now.toInstant(timeZone).toEpochMilliseconds()
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
@@ -129,6 +133,7 @@ class SyncChaptersWithSource(
|
||||
chapterNumber = chapter.chapterNumber,
|
||||
scanlator = chapter.scanlator,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
memo = chapter.memo,
|
||||
)
|
||||
|
||||
if (chapter.dateUpload != 0L) {
|
||||
@@ -144,6 +149,7 @@ class SyncChaptersWithSource(
|
||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
timeZone,
|
||||
now,
|
||||
fetchWindow,
|
||||
)
|
||||
@@ -216,7 +222,7 @@ class SyncChaptersWithSource(
|
||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
updateManga.awaitUpdateFetchInterval(manga, timeZone, now, fetchWindow)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Clock
|
||||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
@@ -22,25 +24,31 @@ class UpdateManga(
|
||||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||
timeZone: TimeZone = TimeZone.currentSystemDefault(),
|
||||
dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone),
|
||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime.date, timeZone),
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
fetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||
fetchInterval.toMangaUpdate(manga, dateTime, timeZone, window),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Clock.System.now().toEpochMilliseconds()))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = mangaId,
|
||||
coverLastModified = Clock.System.now().toEpochMilliseconds(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||
val dateAdded = when (favorite) {
|
||||
true -> Instant.now().toEpochMilli()
|
||||
true -> Clock.System.now().toEpochMilliseconds()
|
||||
false -> 0
|
||||
}
|
||||
return mangaRepository.update(
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import kotlinx.datetime.TimeZone
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
@@ -18,7 +19,6 @@ import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class AddTracks(
|
||||
private val insertTrack: InsertTrack,
|
||||
@@ -62,8 +62,8 @@ class AddTracks(
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
TimeZone.currentSystemDefault(),
|
||||
TimeZone.UTC,
|
||||
)
|
||||
track = track.copy(
|
||||
startDate = startDate,
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.domain.track.service
|
||||
import eu.kanade.domain.track.model.AutoTrackState
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.mangabaka.MangaBaka
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
@@ -16,6 +17,11 @@ class TrackPreferences(
|
||||
"",
|
||||
)
|
||||
|
||||
fun trackDisplayUsername(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_displayname_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||
"",
|
||||
@@ -36,6 +42,8 @@ class TrackPreferences(
|
||||
|
||||
val anilistScoreType: Preference<String> = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
val mangabakaScoreType: Preference<String> = preferenceStore.getString("mangabaka_score_type", MangaBaka.STEP_1)
|
||||
|
||||
val autoUpdateTrack: Preference<Boolean> = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
val autoUpdateTrackOnMarkRead: Preference<AutoTrackState> = preferenceStore.getEnum(
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||
YINYANG(MR.strings.theme_yinyang),
|
||||
YOTSUBA(MR.strings.theme_yotsuba),
|
||||
TOKYONIGHT(MR.strings.theme_tokyonight),
|
||||
MONOCHROME(MR.strings.theme_monochrome),
|
||||
|
||||
// Deprecated
|
||||
|
||||
@@ -53,7 +53,7 @@ import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -66,7 +66,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: ExtensionDetailsScreenModel.State,
|
||||
state: ExtensionDetailsViewModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
|
||||
@@ -48,12 +48,11 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -71,7 +70,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun ExtensionScreen(
|
||||
state: ExtensionsScreenModel.State,
|
||||
state: ExtensionsViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
searchQuery: String?,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
@@ -133,7 +132,7 @@ fun ExtensionScreen(
|
||||
|
||||
@Composable
|
||||
private fun ExtensionContent(
|
||||
state: ExtensionsScreenModel.State,
|
||||
state: ExtensionsViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
@@ -187,14 +186,14 @@ private fun ExtensionContent(
|
||||
}
|
||||
ExtensionHeader(
|
||||
textRes = header.textRes,
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = header.text,
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +211,7 @@ private fun ExtensionContent(
|
||||
},
|
||||
) { item ->
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@@ -20,7 +20,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
state: SearchScreenModel.State,
|
||||
state: SearchViewModel.State,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
|
||||
@@ -4,14 +4,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchScreen(
|
||||
state: SearchScreenModel.State,
|
||||
state: SearchViewModel.State,
|
||||
fromSourceId: Long?,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceViewModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -43,7 +43,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun MigrateSourceScreen(
|
||||
state: MigrateSourceScreenModel.State,
|
||||
state: MigrateSourceViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
|
||||
@@ -10,8 +10,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -23,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
@Composable
|
||||
fun SourcesFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
state: SourcesFilterViewModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
@@ -55,7 +54,7 @@ fun SourcesFilterScreen(
|
||||
@Composable
|
||||
private fun SourcesFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
state: SourcesFilterViewModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
@@ -69,7 +68,7 @@ private fun SourcesFilterContent(
|
||||
contentType = "source-filter-header",
|
||||
) {
|
||||
SourcesFilterHeader(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
language = language,
|
||||
enabled = enabled,
|
||||
onClickItem = onClickLanguage,
|
||||
@@ -82,7 +81,7 @@ private fun SourcesFilterContent(
|
||||
contentType = { "source-filter-item" },
|
||||
) { source ->
|
||||
SourcesFilterItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
source = source,
|
||||
enabled = "${source.id}" !in state.disabledSources,
|
||||
onClickItem = onClickSource,
|
||||
|
||||
@@ -21,8 +21,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesViewModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Source
|
||||
@@ -40,7 +40,7 @@ import tachiyomi.source.local.isLocal
|
||||
|
||||
@Composable
|
||||
fun SourcesScreen(
|
||||
state: SourcesScreenModel.State,
|
||||
state: SourcesViewModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source, Listing) -> Unit,
|
||||
onClickPin: (Source) -> Unit,
|
||||
|
||||
@@ -27,13 +27,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.core.common.preference.CheckboxState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun CategoryCreateDialog(
|
||||
@@ -91,8 +89,6 @@ fun CategoryCreateDialog(
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
@@ -155,8 +151,6 @@ fun CategoryRenameDialog(
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,23 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
dateEpochMillis: Long,
|
||||
): String {
|
||||
return relativeDateText(
|
||||
localDate = LocalDate.ofInstant(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
localDate = Instant.fromEpochMilliseconds(dateEpochMillis)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date
|
||||
.takeIf { dateEpochMillis != 0L },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ fun TabbedDialog(
|
||||
modifier = Modifier.weight(1f),
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
divider = {},
|
||||
) {
|
||||
tabTitles.fastForEachIndexed { index, tab ->
|
||||
Tab(
|
||||
@@ -72,7 +71,6 @@ fun TabbedDialog(
|
||||
|
||||
tabOverflowMenuContent?.let { MoreMenu(it) }
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
HorizontalPager(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
|
||||
@@ -18,8 +18,8 @@ import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.history.components.HistoryItem
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
|
||||
import kotlinx.datetime.LocalDate
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
@@ -28,17 +28,16 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
fun HistoryScreen(
|
||||
state: HistoryScreenModel.State,
|
||||
state: HistoryViewModel.State,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
onClickCover: (mangaId: Long) -> Unit,
|
||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||
onClickFavorite: (mangaId: Long) -> Unit,
|
||||
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
||||
onDialogChange: (HistoryViewModel.Dialog?) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
@@ -53,7 +52,7 @@ fun HistoryScreen(
|
||||
title = stringResource(MR.strings.pref_clear_history),
|
||||
icon = Icons.Outlined.DeleteSweep,
|
||||
onClick = {
|
||||
onDialogChange(HistoryScreenModel.Dialog.DeleteAll)
|
||||
onDialogChange(HistoryViewModel.Dialog.DeleteAll)
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -83,7 +82,7 @@ fun HistoryScreen(
|
||||
contentPadding = contentPadding,
|
||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
||||
onClickDelete = { item -> onDialogChange(HistoryViewModel.Dialog.Delete(item)) },
|
||||
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
|
||||
)
|
||||
}
|
||||
@@ -116,14 +115,14 @@ private fun HistoryScreenContent(
|
||||
when (item) {
|
||||
is HistoryUiModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is HistoryUiModel.Item -> {
|
||||
val value = item.item
|
||||
HistoryItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
history = value,
|
||||
onClickCover = { onClickCover(value) },
|
||||
onClickResume = { onClickResume(value) },
|
||||
@@ -144,8 +143,8 @@ sealed interface HistoryUiModel {
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
internal fun HistoryScreenPreviews(
|
||||
@PreviewParameter(HistoryScreenModelStateProvider::class)
|
||||
historyState: HistoryScreenModel.State,
|
||||
@PreviewParameter(HistoryviewModelStateProvider::class)
|
||||
historyState: HistoryViewModel.State,
|
||||
) {
|
||||
TachiyomiPreviewTheme {
|
||||
HistoryScreen(
|
||||
|
||||
+22
-17
@@ -1,32 +1,35 @@
|
||||
package eu.kanade.presentation.history
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Instant
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenModel.State> {
|
||||
class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.State> {
|
||||
|
||||
private val multiPage = HistoryScreenModel.State(
|
||||
private val multiPage = HistoryViewModel.State(
|
||||
searchQuery = null,
|
||||
list =
|
||||
listOf(HistoryUiModelExamples.headerToday)
|
||||
.asSequence()
|
||||
.plus(HistoryUiModelExamples.items().take(3))
|
||||
.plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) })
|
||||
.plus(HistoryUiModelExamples.header { it.minus(1.days) })
|
||||
.plus(HistoryUiModelExamples.items().take(1))
|
||||
.plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) })
|
||||
.plus(HistoryUiModelExamples.header { it.minus(2.days) })
|
||||
.plus(HistoryUiModelExamples.items().take(7))
|
||||
.toList(),
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val shortRecent = HistoryScreenModel.State(
|
||||
private val shortRecent = HistoryViewModel.State(
|
||||
searchQuery = null,
|
||||
list = listOf(
|
||||
HistoryUiModelExamples.headerToday,
|
||||
@@ -35,7 +38,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val shortFuture = HistoryScreenModel.State(
|
||||
private val shortFuture = HistoryViewModel.State(
|
||||
searchQuery = null,
|
||||
list = listOf(
|
||||
HistoryUiModelExamples.headerTomorrow,
|
||||
@@ -44,23 +47,23 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val empty = HistoryScreenModel.State(
|
||||
private val empty = HistoryViewModel.State(
|
||||
searchQuery = null,
|
||||
list = listOf(),
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val loadingWithSearchQuery = HistoryScreenModel.State(
|
||||
private val loadingWithSearchQuery = HistoryViewModel.State(
|
||||
searchQuery = "Example Search Query",
|
||||
)
|
||||
|
||||
private val loading = HistoryScreenModel.State(
|
||||
private val loading = HistoryViewModel.State(
|
||||
searchQuery = null,
|
||||
list = null,
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
override val values: Sequence<HistoryScreenModel.State> = sequenceOf(
|
||||
override val values: Sequence<HistoryViewModel.State> = sequenceOf(
|
||||
multiPage,
|
||||
shortRecent,
|
||||
shortFuture,
|
||||
@@ -72,10 +75,12 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
private object HistoryUiModelExamples {
|
||||
val headerToday = header()
|
||||
val headerTomorrow =
|
||||
HistoryUiModel.Header(LocalDate.now().plusDays(1))
|
||||
HistoryUiModel.Header(Clock.System.now().plus(1.days).toLocalDateTime(TimeZone.currentSystemDefault()).date)
|
||||
|
||||
fun header(instantBuilder: (Instant) -> Instant = { it }) =
|
||||
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
|
||||
HistoryUiModel.Header(
|
||||
instantBuilder(Clock.System.now()).toLocalDateTime(TimeZone.currentSystemDefault()).date,
|
||||
)
|
||||
|
||||
fun items() = sequence {
|
||||
var count = 1
|
||||
@@ -94,7 +99,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
mangaId = Random.nextLong(),
|
||||
title = "Test Title",
|
||||
chapterNumber = Random.nextDouble(),
|
||||
readAt = Date.from(Instant.now()),
|
||||
readAt = Date.from(Clock.System.now().toJavaInstant()),
|
||||
readDuration = Random.nextLong(),
|
||||
coverData = MangaCover(
|
||||
mangaId = Random.nextLong(),
|
||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
|
||||
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.ui.library.LibrarySettingsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
@@ -41,7 +41,7 @@ import tachiyomi.presentation.core.util.collectAsState
|
||||
@Composable
|
||||
fun LibrarySettingsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
viewModel: LibrarySettingsViewModel,
|
||||
category: Category?,
|
||||
) {
|
||||
TabbedDialog(
|
||||
@@ -59,14 +59,14 @@ fun LibrarySettingsDialog(
|
||||
) {
|
||||
when (page) {
|
||||
0 -> FilterPage(
|
||||
screenModel = screenModel,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
1 -> SortPage(
|
||||
category = category,
|
||||
screenModel = screenModel,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
2 -> DisplayPage(
|
||||
screenModel = screenModel,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,11 @@ fun LibrarySettingsDialog(
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterPage(
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
viewModel: LibrarySettingsViewModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded.collectAsState()
|
||||
val downloadedOnly by screenModel.preferences.downloadedOnly.collectAsState()
|
||||
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
|
||||
val filterDownloaded by viewModel.libraryPreferences.filterDownloaded.collectAsState()
|
||||
val downloadedOnly by viewModel.preferences.downloadedOnly.collectAsState()
|
||||
val autoUpdateMangaRestrictions by viewModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
|
||||
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
@@ -89,64 +89,64 @@ private fun ColumnScope.FilterPage(
|
||||
filterDownloaded
|
||||
},
|
||||
enabled = !downloadedOnly,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterDownloaded) },
|
||||
)
|
||||
val filterUnread by screenModel.libraryPreferences.filterUnread.collectAsState()
|
||||
val filterUnread by viewModel.libraryPreferences.filterUnread.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterUnread) },
|
||||
)
|
||||
val filterStarted by screenModel.libraryPreferences.filterStarted.collectAsState()
|
||||
val filterStarted by viewModel.libraryPreferences.filterStarted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterStarted) },
|
||||
)
|
||||
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked.collectAsState()
|
||||
val filterBookmarked by viewModel.libraryPreferences.filterBookmarked.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterBookmarked) },
|
||||
)
|
||||
val filterCompleted by screenModel.libraryPreferences.filterCompleted.collectAsState()
|
||||
val filterCompleted by viewModel.libraryPreferences.filterCompleted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.completed),
|
||||
state = filterCompleted,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterCompleted) },
|
||||
)
|
||||
// TODO: re-enable when custom intervals are ready for stable
|
||||
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
|
||||
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom.collectAsState()
|
||||
val filterIntervalCustom by viewModel.libraryPreferences.filterIntervalCustom.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_interval_custom),
|
||||
state = filterIntervalCustom,
|
||||
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||
onClick = { viewModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
|
||||
)
|
||||
}
|
||||
|
||||
val trackers by screenModel.trackersFlow.collectAsState()
|
||||
val trackers by viewModel.trackersFlow.collectAsState()
|
||||
when (trackers.size) {
|
||||
0 -> {
|
||||
// No trackers
|
||||
}
|
||||
1 -> {
|
||||
val service = trackers[0]
|
||||
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||
val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_tracked),
|
||||
state = filterTracker,
|
||||
onClick = { screenModel.toggleTracker(service.id.toInt()) },
|
||||
onClick = { viewModel.toggleTracker(service.id.toInt()) },
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
HeadingItem(MR.strings.action_filter_tracked)
|
||||
trackers.map { service ->
|
||||
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||
val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
|
||||
TriStateItem(
|
||||
label = service.name,
|
||||
state = filterTracker,
|
||||
onClick = { screenModel.toggleTracker(service.id.toInt()) },
|
||||
onClick = { viewModel.toggleTracker(service.id.toInt()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -156,9 +156,9 @@ private fun ColumnScope.FilterPage(
|
||||
@Composable
|
||||
private fun ColumnScope.SortPage(
|
||||
category: Category?,
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
viewModel: LibrarySettingsViewModel,
|
||||
) {
|
||||
val trackers by screenModel.trackersFlow.collectAsState()
|
||||
val trackers by viewModel.trackersFlow.collectAsState()
|
||||
val sortingMode = category.sort.type
|
||||
val sortDescending = !category.sort.isAscending
|
||||
|
||||
@@ -189,7 +189,7 @@ private fun ColumnScope.SortPage(
|
||||
icon = Icons.Default.Refresh
|
||||
.takeIf { sortingMode == LibrarySort.Type.Random },
|
||||
onClick = {
|
||||
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
|
||||
viewModel.setSort(category, mode, LibrarySort.Direction.Ascending)
|
||||
},
|
||||
)
|
||||
return@map
|
||||
@@ -211,7 +211,7 @@ private fun ColumnScope.SortPage(
|
||||
LibrarySort.Direction.Ascending
|
||||
}
|
||||
}
|
||||
screenModel.setSort(category, mode, direction)
|
||||
viewModel.setSort(category, mode, direction)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -226,14 +226,14 @@ private val displayModes = listOf(
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DisplayPage(
|
||||
screenModel: LibrarySettingsScreenModel,
|
||||
viewModel: LibrarySettingsViewModel,
|
||||
) {
|
||||
val displayMode by screenModel.libraryPreferences.displayMode.collectAsState()
|
||||
val displayMode by viewModel.libraryPreferences.displayMode.collectAsState()
|
||||
SettingsChipRow(MR.strings.action_display_mode) {
|
||||
displayModes.map { (titleRes, mode) ->
|
||||
FilterChip(
|
||||
selected = displayMode == mode,
|
||||
onClick = { screenModel.setDisplayMode(mode) },
|
||||
onClick = { viewModel.setDisplayMode(mode) },
|
||||
label = { Text(stringResource(titleRes)) },
|
||||
)
|
||||
}
|
||||
@@ -243,9 +243,9 @@ private fun ColumnScope.DisplayPage(
|
||||
val configuration = LocalConfiguration.current
|
||||
val columnPreference = remember {
|
||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
screenModel.libraryPreferences.landscapeColumns
|
||||
viewModel.libraryPreferences.landscapeColumns
|
||||
} else {
|
||||
screenModel.libraryPreferences.portraitColumns
|
||||
viewModel.libraryPreferences.portraitColumns
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,32 +267,32 @@ private fun ColumnScope.DisplayPage(
|
||||
HeadingItem(MR.strings.overlay_header)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_download_badge),
|
||||
pref = screenModel.libraryPreferences.downloadBadge,
|
||||
pref = viewModel.libraryPreferences.downloadBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_unread_badge),
|
||||
pref = screenModel.libraryPreferences.unreadBadge,
|
||||
pref = viewModel.libraryPreferences.unreadBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_local_badge),
|
||||
pref = screenModel.libraryPreferences.localBadge,
|
||||
pref = viewModel.libraryPreferences.localBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_language_badge),
|
||||
pref = screenModel.libraryPreferences.languageBadge,
|
||||
pref = viewModel.libraryPreferences.languageBadge,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_continue_reading_button),
|
||||
pref = screenModel.libraryPreferences.showContinueReadingButton,
|
||||
pref = viewModel.libraryPreferences.showContinueReadingButton,
|
||||
)
|
||||
|
||||
HeadingItem(MR.strings.tabs_header)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_tabs),
|
||||
pref = screenModel.libraryPreferences.categoryTabs,
|
||||
pref = viewModel.libraryPreferences.categoryTabs,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_number_of_items),
|
||||
pref = screenModel.libraryPreferences.categoryNumberOfItems,
|
||||
pref = viewModel.libraryPreferences.categoryNumberOfItems,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package eu.kanade.presentation.library.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
@@ -22,29 +18,22 @@ internal fun LibraryTabs(
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||
Column(modifier = Modifier.zIndex(2f)) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = currentPageIndex,
|
||||
edgePadding = 0.dp,
|
||||
// TODO: use default when width is fixed upstream
|
||||
// https://issuetracker.google.com/issues/242879624
|
||||
divider = {},
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
Tab(
|
||||
selected = currentPageIndex == index,
|
||||
onClick = { onTabItemClick(index) },
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
badgeCount = getItemCountForCategory(category),
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = currentPageIndex,
|
||||
edgePadding = 0.dp,
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
Tab(
|
||||
selected = currentPageIndex == index,
|
||||
onClick = { onTabItemClick(index) },
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
badgeCount = getItemCountForCategory(category),
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ import eu.kanade.presentation.util.formatChapterNumber
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaViewModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.service.missingChaptersCount
|
||||
@@ -75,11 +75,11 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.shouldExpandFAB
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.time.Instant
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
state: MangaViewModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
nextUpdate: Instant?,
|
||||
isTabletUi: Boolean,
|
||||
@@ -210,7 +210,7 @@ fun MangaScreen(
|
||||
|
||||
@Composable
|
||||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
state: MangaViewModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
@@ -452,7 +452,7 @@ private fun MangaScreenSmallImpl(
|
||||
|
||||
@Composable
|
||||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
state: MangaViewModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
nextUpdate: Instant?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
||||
@@ -68,7 +68,7 @@ fun MangaCoverDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false, // Doesn't work https://issuetracker.google.com/issues/246909281
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
) {
|
||||
Scaffold(
|
||||
|
||||
@@ -20,15 +20,17 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.daysUntil
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Composable
|
||||
fun DeleteChaptersDialog(
|
||||
@@ -72,8 +74,8 @@ fun SetIntervalDialog(
|
||||
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
val now = Clock.System.now()
|
||||
now.daysUntil(nextUpdate, TimeZone.currentSystemDefault()).coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.daysUntil
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.ast.findChildOfType
|
||||
@@ -105,9 +107,9 @@ import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Composable
|
||||
fun MangaInfoBox(
|
||||
@@ -189,8 +191,8 @@ fun MangaActionRow(
|
||||
// TODO: show something better when using custom interval
|
||||
val nextUpdateDays = remember(nextUpdate) {
|
||||
return@remember if (nextUpdate != null) {
|
||||
val now = Instant.now()
|
||||
now.until(nextUpdate, ChronoUnit.DAYS).toInt().coerceAtLeast(0)
|
||||
val now = Clock.System.now()
|
||||
now.daysUntil(nextUpdate, TimeZone.currentSystemDefault()).coerceAtLeast(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import eu.kanade.presentation.manga.components.MarkdownRender
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreenModel
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@@ -27,16 +29,27 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
||||
fun NewUpdateScreen(
|
||||
versionName: String,
|
||||
changelogInfo: String,
|
||||
stage: NewUpdateScreenModel.Stage,
|
||||
downloadProgress: () -> Int,
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRejectUpdate: () -> Unit,
|
||||
onAcceptUpdate: () -> Unit,
|
||||
onRejectUpdate: () -> Unit,
|
||||
) {
|
||||
InfoScreen(
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
headingText = stringResource(MR.strings.update_check_notification_update_available),
|
||||
subtitleText = versionName,
|
||||
acceptText = stringResource(MR.strings.update_check_confirm),
|
||||
acceptText = when (stage) {
|
||||
NewUpdateScreenModel.Stage.Available -> stringResource(MR.strings.update_check_confirm)
|
||||
NewUpdateScreenModel.Stage.Downloading -> stringResource(
|
||||
MR.strings.downloading_with_progress,
|
||||
downloadProgress(),
|
||||
)
|
||||
NewUpdateScreenModel.Stage.Downloaded -> stringResource(MR.strings.action_install)
|
||||
NewUpdateScreenModel.Stage.Failed -> stringResource(MR.strings.action_retry)
|
||||
},
|
||||
onAcceptClick = onAcceptUpdate,
|
||||
canAccept = stage != NewUpdateScreenModel.Stage.Downloading,
|
||||
rejectText = stringResource(MR.strings.action_not_now),
|
||||
onRejectClick = onRejectUpdate,
|
||||
) {
|
||||
@@ -47,7 +60,7 @@ fun NewUpdateScreen(
|
||||
) {
|
||||
MarkdownRender(
|
||||
content = changelogInfo,
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
flavour = remember { GFMFlavourDescriptor() },
|
||||
)
|
||||
|
||||
TextButton(
|
||||
@@ -76,9 +89,11 @@ private fun NewUpdateScreenPreview() {
|
||||
- Hello
|
||||
- World
|
||||
""".trimIndent(),
|
||||
stage = NewUpdateScreenModel.Stage.Available,
|
||||
downloadProgress = { 0 },
|
||||
onOpenInBrowser = {},
|
||||
onRejectUpdate = {},
|
||||
onAcceptUpdate = {},
|
||||
onRejectUpdate = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +169,6 @@ internal class PermissionStep : OnboardingStep {
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
trailingContent = {
|
||||
OutlinedButton(
|
||||
enabled = !granted,
|
||||
@@ -187,7 +185,9 @@ internal class PermissionStep : OnboardingStep {
|
||||
}
|
||||
}
|
||||
},
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
content = { Text(text = title) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,15 +201,15 @@ internal class PermissionStep : OnboardingStep {
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = granted,
|
||||
onCheckedChange = onToggleChange,
|
||||
)
|
||||
},
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
content = { Text(text = title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,12 +103,13 @@ sealed class Preference {
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
* Multiple entries can be selected at the same time.
|
||||
*/
|
||||
data class MultiSelectListPreference(
|
||||
val preference: PreferenceData<Set<String>>,
|
||||
val entries: Map<String, String>,
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class MultiSelectListPreference<T>(
|
||||
val preference: PreferenceData<Set<T>>,
|
||||
val entries: Map<T, String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
val subtitleProvider: @Composable (value: Set<String>, entries: Map<String, String>) -> String? =
|
||||
val subtitleProvider: @Composable (value: Set<T>, entries: Map<T, String>) -> String? =
|
||||
{ v, e ->
|
||||
val combined = remember(v, e) {
|
||||
v.mapNotNull { e[it] }
|
||||
@@ -120,8 +121,15 @@ sealed class Preference {
|
||||
},
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<String>, Boolean>()
|
||||
override val onValueChanged: suspend (value: Set<T>) -> Boolean = { true },
|
||||
) : PreferenceItem<Set<T>, Boolean>() {
|
||||
internal fun internalSet(value: Set<Any?>) = preference.set(value as Set<T>)
|
||||
internal suspend fun internalOnValueChanged(value: Set<Any?>) = onValueChanged(value as Set<T>)
|
||||
|
||||
@Composable
|
||||
internal fun internalSubtitleProvider(value: Set<Any?>, entries: Map<out Any?, String>) =
|
||||
subtitleProvider(value as Set<T>, entries as Map<T, String>)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
|
||||
@@ -128,15 +128,18 @@ internal fun PreferenceItem(
|
||||
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
||||
is Preference.PreferenceItem.MultiSelectListPreference<*> -> {
|
||||
val values by item.preference.collectAsState()
|
||||
MultiSelectListPreferenceWidget(
|
||||
preference = item,
|
||||
values = values,
|
||||
title = item.title,
|
||||
subtitle = item.internalSubtitleProvider(values, item.entries),
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValuesChange = { newValues ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValues)) {
|
||||
item.preference.set(newValues.toMutableSet())
|
||||
if (item.internalOnValueChanged(newValues)) {
|
||||
item.internalSet(newValues)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -171,7 +174,7 @@ internal fun PreferenceItem(
|
||||
}
|
||||
TrackingPreferenceWidget(
|
||||
tracker = item.tracker,
|
||||
checked = isLoggedIn,
|
||||
isLoggedIn = isLoggedIn,
|
||||
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
||||
)
|
||||
}
|
||||
|
||||
+5
-2
@@ -19,12 +19,15 @@ import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Clock
|
||||
|
||||
object SettingsAppearanceScreen : SearchableSettings {
|
||||
|
||||
@@ -99,7 +102,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val now = remember { LocalDate.now() }
|
||||
val now = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat.collectAsState()
|
||||
val formattedNow = remember(dateFormat) {
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||
private fun getExcludedCategoriesPreference(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
): Preference.PreferenceItem.MultiSelectListPreference<String> {
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = downloadPreferences.removeExcludeCategories,
|
||||
entries = categories()
|
||||
|
||||
+24
-10
@@ -71,7 +71,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
@Composable
|
||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val fullscreen by readerPreferences.fullscreen.collectAsState()
|
||||
val verticalNavigatorForLongStrip by readerPreferences.verticalNavigatorForLongStrip.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
@@ -108,15 +107,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
preference = readerPreferences.showPageNumber,
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorForLongStrip,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorOnLeft,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
enabled = verticalNavigatorForLongStrip,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -392,6 +382,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
private fun getNavigationGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys
|
||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
|
||||
val verticalNavigator by readerPreferences.verticalNavigator.collectAsState()
|
||||
val verticalNavigatorHeightPref = readerPreferences.verticalNavigatorHeight
|
||||
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_reader_navigation),
|
||||
preferenceItems = listOf(
|
||||
@@ -404,6 +399,25 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
preference = readerPreferences.verticalNavigator,
|
||||
entries = ReadingMode.entries.filter { it != ReadingMode.DEFAULT }
|
||||
.associate { it to stringResource(it.stringRes) },
|
||||
title = stringResource(MR.strings.pref_vertical_navigator),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.verticalNavigatorOnLeft,
|
||||
title = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
enabled = verticalNavigator.isNotEmpty(),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = verticalNavigatorHeight,
|
||||
valueRange = 65..100,
|
||||
steps = 6,
|
||||
title = stringResource(MR.strings.pref_vertical_navigator_height),
|
||||
onValueChanged = { verticalNavigatorHeightPref.set(it) },
|
||||
enabled = verticalNavigator.isNotEmpty(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+12
@@ -51,6 +51,8 @@ import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||
import eu.kanade.tachiyomi.data.track.hikka.HikkaApi
|
||||
import eu.kanade.tachiyomi.data.track.mangabaka.MangaBakaApi
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
@@ -136,6 +138,11 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.services),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
tracker = trackerManager.mangaBaka,
|
||||
login = { context.openInBrowser(MangaBakaApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.mangaBaka) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
tracker = trackerManager.myAnimeList,
|
||||
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
||||
@@ -166,6 +173,11 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackerPreference(
|
||||
tracker = trackerManager.hikka,
|
||||
login = { context.openInBrowser(HikkaApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackerManager.hikka) },
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.tracking_info)),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -40,6 +40,8 @@ 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 kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
@@ -59,9 +61,7 @@ import tachiyomi.presentation.core.icons.Reddit
|
||||
import tachiyomi.presentation.core.icons.X
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.Instant
|
||||
|
||||
object AboutScreen : Screen() {
|
||||
|
||||
@@ -219,7 +219,7 @@ object AboutScreen : Screen() {
|
||||
val updateChecker = AppUpdateChecker()
|
||||
withUIContext {
|
||||
try {
|
||||
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
|
||||
when (val result = withIOContext { updateChecker.checkForUpdate(forceCheck = true) }) {
|
||||
is GetApplicationRelease.Result.NewUpdate -> {
|
||||
onAvailableUpdate(result)
|
||||
}
|
||||
@@ -273,16 +273,14 @@ object AboutScreen : Screen() {
|
||||
|
||||
internal fun getFormattedBuildTime(): String {
|
||||
return try {
|
||||
LocalDateTime.ofInstant(
|
||||
Instant.parse(BuildConfig.BUILD_TIME),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
Instant.parse(BuildConfig.BUILD_TIME)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat.get(),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
BuildConfig.BUILD_TIME
|
||||
}
|
||||
}
|
||||
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
package eu.kanade.presentation.more.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
class OpenSourceLibraryLicenseScreen(
|
||||
private val name: String,
|
||||
private val website: String?,
|
||||
private val license: String,
|
||||
) : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = name,
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
if (!website.isNullOrEmpty()) {
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.website),
|
||||
icon = Icons.Default.Public,
|
||||
onClick = { uriHandler.openUri(website) },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = it,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(contentPadding)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
HtmlLicenseText(html = license)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HtmlLicenseText(html: String) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
MaterialTextView(it)
|
||||
},
|
||||
update = {
|
||||
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-10
@@ -8,7 +8,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
||||
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -36,15 +36,7 @@ class OpenSourceLicensesScreen : Screen() {
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
onLibraryClick = {
|
||||
navigator.push(
|
||||
OpenSourceLibraryLicenseScreen(
|
||||
name = it.name,
|
||||
website = it.website,
|
||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
detailMode = LibraryDetailMode.Sheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-18
@@ -30,9 +30,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
@@ -42,6 +41,7 @@ import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.core.common.util.lang.toLong
|
||||
@@ -67,13 +67,13 @@ class ClearDatabaseScreen : Screen() {
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { ClearDatabaseScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<ClearDatabaseViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
when (val s = state) {
|
||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseScreenModel.State.Ready -> {
|
||||
is ClearDatabaseViewModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseViewModel.State.Ready -> {
|
||||
if (s.showConfirmation) {
|
||||
var keepReadManga by remember { mutableStateOf(true) }
|
||||
AlertDialog(
|
||||
@@ -108,14 +108,14 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = model::hideConfirmation,
|
||||
onDismissRequest = viewModel::hideConfirmation,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launchUI {
|
||||
model.removeMangaBySourceId(keepReadManga)
|
||||
model.clearSelection()
|
||||
model.hideConfirmation()
|
||||
viewModel.removeMangaBySourceId(keepReadManga)
|
||||
viewModel.clearSelection()
|
||||
viewModel.hideConfirmation()
|
||||
context.toast(MR.strings.clear_database_completed)
|
||||
}
|
||||
},
|
||||
@@ -124,7 +124,7 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = model::hideConfirmation) {
|
||||
TextButton(onClick = viewModel::hideConfirmation) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
@@ -143,12 +143,12 @@ class ClearDatabaseScreen : Screen() {
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = model::selectAll,
|
||||
onClick = viewModel::selectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = model::invertSelection,
|
||||
onClick = viewModel::invertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -168,14 +168,14 @@ class ClearDatabaseScreen : Screen() {
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_delete),
|
||||
actionEnabled = s.selection.isNotEmpty(),
|
||||
onClickAction = model::showConfirmation,
|
||||
onClickAction = viewModel::showConfirmation,
|
||||
) {
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
source = sourceWithCount.source,
|
||||
count = sourceWithCount.count,
|
||||
isSelected = s.selection.contains(sourceWithCount.id),
|
||||
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
|
||||
onClickSelect = { viewModel.toggleSelection(sourceWithCount.source) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -220,12 +220,12 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {
|
||||
class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(State.Loading) {
|
||||
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
|
||||
private val database: Database = Injekt.get()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
getSourcesWithNonLibraryManga.subscribe()
|
||||
.collectLatest { list ->
|
||||
mutableState.update { old ->
|
||||
|
||||
+6
-6
@@ -71,12 +71,6 @@ class AppLanguageScreen : Screen() {
|
||||
modifier = Modifier.clickable {
|
||||
currentLanguage = it.langTag
|
||||
},
|
||||
headlineContent = { Text(it.displayName) },
|
||||
supportingContent = {
|
||||
it.localizedDisplayName?.let {
|
||||
Text(it)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if (currentLanguage == it.langTag) {
|
||||
Icon(
|
||||
@@ -86,6 +80,12 @@ class AppLanguageScreen : Screen() {
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingContent = {
|
||||
it.localizedDisplayName?.let {
|
||||
Text(it)
|
||||
}
|
||||
},
|
||||
content = { Text(it.displayName) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog
|
||||
@@ -26,11 +26,11 @@ class ExtensionStoresScreen(
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
val viewModel = viewModel<ExtensionStoresViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(url) {
|
||||
url?.let { screenModel.addFromDeeplink(url) }
|
||||
url?.let { viewModel.addFromDeeplink(url) }
|
||||
}
|
||||
|
||||
if (state is ExtensionStoreScreenState.Loading) {
|
||||
@@ -42,12 +42,12 @@ class ExtensionStoresScreen(
|
||||
|
||||
ExtensionStoresScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||
onClickCreate = { viewModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
|
||||
onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
|
||||
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
|
||||
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||
onClickRefresh = { screenModel.refreshRepos() },
|
||||
onClickDelete = { viewModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||
onClickRefresh = { viewModel.refreshRepos() },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
@@ -55,8 +55,8 @@ class ExtensionStoresScreen(
|
||||
null -> {}
|
||||
is ExtensionStoreDialog.Create -> {
|
||||
ExtensionStoreCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onCreate = { viewModel.createRepo(it) },
|
||||
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
|
||||
processing = dialog.processing,
|
||||
errorMessage = dialog.errorMessage,
|
||||
@@ -64,16 +64,16 @@ class ExtensionStoresScreen(
|
||||
}
|
||||
is ExtensionStoreDialog.Delete -> {
|
||||
ExtensionStoreDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onDelete = { viewModel.deleteRepo(dialog.store.indexUrl) },
|
||||
storeName = dialog.store.name,
|
||||
storeIndexUrl = dialog.store.indexUrl,
|
||||
)
|
||||
}
|
||||
is ExtensionStoreDialog.Confirm -> {
|
||||
ExtensionStoreConfirmDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(dialog.url) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onCreate = { viewModel.createRepo(dialog.url) },
|
||||
storeIndexUrl = dialog.url,
|
||||
storeAlreadyExists = dialog.alreadyExists,
|
||||
processing = dialog.processing,
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import mihon.domain.extension.interactor.AddExtensionStore
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||
@@ -16,13 +16,13 @@ import tachiyomi.core.common.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionStoresScreenModel(
|
||||
class ExtensionStoresViewModel(
|
||||
private val getExtensionStores: GetExtensionStores = Injekt.get(),
|
||||
private val addExtensionStore: AddExtensionStore = Injekt.get(),
|
||||
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
|
||||
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||
) : StateViewModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||
|
||||
private inline fun updateSuccessState(
|
||||
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
|
||||
@@ -36,7 +36,7 @@ class ExtensionStoresScreenModel(
|
||||
}
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
getExtensionStores.subscribe()
|
||||
.collectLatest { stores ->
|
||||
mutableState.update {
|
||||
@@ -55,7 +55,7 @@ class ExtensionStoresScreenModel(
|
||||
* @param baseUrl The baseUrl of the repo to create.
|
||||
*/
|
||||
fun createRepo(baseUrl: String) {
|
||||
screenModelScope.launch {
|
||||
viewModelScope.launch {
|
||||
updateSuccessState {
|
||||
it.copy(
|
||||
dialog = when (it.dialog) {
|
||||
@@ -97,7 +97,7 @@ class ExtensionStoresScreenModel(
|
||||
val status = state.value
|
||||
|
||||
if (status is ExtensionStoreScreenState.Success) {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
updateExtensionStores()
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ class ExtensionStoresScreenModel(
|
||||
* Deletes the given repo from the database
|
||||
*/
|
||||
fun deleteRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
removeExtensionStore(baseUrl)
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
-4
@@ -20,10 +20,8 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoreCreateDialog(
|
||||
@@ -94,8 +92,6 @@ fun ExtensionStoreCreateDialog(
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -11,8 +11,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
@@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
@@ -37,8 +37,8 @@ class CreateBackupScreen : Screen() {
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { CreateBackupScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<CreateBackupViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val chooseBackupDir = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/*"),
|
||||
@@ -49,7 +49,7 @@ class CreateBackupScreen : Screen() {
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
model.createBackup(context, it)
|
||||
viewModel.createBackup(context, it)
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
@@ -87,13 +87,13 @@ class CreateBackupScreen : Screen() {
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_library) {
|
||||
Options(BackupOptions.libraryOptions, state, model)
|
||||
Options(BackupOptions.libraryOptions, state, viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_settings) {
|
||||
Options(BackupOptions.settingsOptions, state, model)
|
||||
Options(BackupOptions.settingsOptions, state, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +103,8 @@ class CreateBackupScreen : Screen() {
|
||||
@Composable
|
||||
private fun Options(
|
||||
options: List<BackupOptions.Entry>,
|
||||
state: CreateBackupScreenModel.State,
|
||||
model: CreateBackupScreenModel,
|
||||
state: CreateBackupViewModel.State,
|
||||
model: CreateBackupViewModel,
|
||||
) {
|
||||
options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
@@ -119,7 +119,7 @@ class CreateBackupScreen : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||
class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) {
|
||||
|
||||
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
|
||||
+32
-10
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -14,14 +15,15 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
@@ -32,6 +34,7 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
@@ -39,6 +42,8 @@ import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class RestoreBackupScreen(
|
||||
private val uri: String,
|
||||
@@ -46,10 +51,14 @@ class RestoreBackupScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<RestoreBackupViewModel>(
|
||||
factory = RestoreBackupViewModel.Factory,
|
||||
extras = CreationExtras {
|
||||
set(RestoreBackupViewModel.URI_KEY, uri)
|
||||
},
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -65,7 +74,7 @@ class RestoreBackupScreen(
|
||||
actionLabel = stringResource(MR.strings.action_restore),
|
||||
actionEnabled = state.canRestore && state.options.canRestore(),
|
||||
onClickAction = {
|
||||
model.startRestore()
|
||||
viewModel.startRestore()
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
@@ -83,7 +92,7 @@ class RestoreBackupScreen(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
viewModel.toggle(option.setter, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -164,10 +173,23 @@ class RestoreBackupScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private class RestoreBackupScreenModel(
|
||||
class RestoreBackupViewModel(
|
||||
private val context: Context,
|
||||
private val uri: String,
|
||||
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||
) : StateViewModel<RestoreBackupViewModel.State>(State()) {
|
||||
|
||||
companion object {
|
||||
val URI_KEY = CreationExtras.Key<String>()
|
||||
|
||||
val Factory = viewModelFactory {
|
||||
initializer {
|
||||
RestoreBackupViewModel(
|
||||
context = Injekt.get<Application>(),
|
||||
uri = get(URI_KEY)!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
validate(uri.toUri())
|
||||
|
||||
+32
-18
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more.settings.screen.debug
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -18,32 +19,34 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.presentation.util.ioCoroutineScope
|
||||
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.Instant
|
||||
|
||||
class WorkerInfoScreen : Screen() {
|
||||
|
||||
@@ -56,10 +59,10 @@ class WorkerInfoScreen : Screen() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { Model(context) }
|
||||
val enqueued by screenModel.enqueued.collectAsState()
|
||||
val finished by screenModel.finished.collectAsState()
|
||||
val running by screenModel.running.collectAsState()
|
||||
val viewModel = viewModel<WorkerInfoViewModel>(factory = WorkerInfoViewModel.Factory)
|
||||
val enqueued by viewModel.enqueued.collectAsState()
|
||||
val finished by viewModel.finished.collectAsState()
|
||||
val running by viewModel.running.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -117,7 +120,20 @@ class WorkerInfoScreen : Screen() {
|
||||
)
|
||||
}
|
||||
|
||||
private class Model(context: Context) : ScreenModel {
|
||||
class WorkerInfoViewModel(
|
||||
context: Context,
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
val Factory = viewModelFactory {
|
||||
initializer {
|
||||
WorkerInfoViewModel(
|
||||
context = Injekt.get<Application>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val workManager = context.workManager
|
||||
|
||||
val finished = workManager
|
||||
@@ -125,17 +141,17 @@ class WorkerInfoScreen : Screen() {
|
||||
WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED),
|
||||
)
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
val running = workManager
|
||||
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
val enqueued = workManager
|
||||
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
|
||||
.map(::constructString)
|
||||
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
|
||||
|
||||
private fun constructString(list: List<WorkInfo>) = buildString {
|
||||
if (list.isEmpty()) {
|
||||
@@ -149,10 +165,8 @@ class WorkerInfoScreen : Screen() {
|
||||
}
|
||||
appendLine("State: ${workInfo.state}")
|
||||
if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
val timestamp = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
val timestamp = Instant.fromEpochMilliseconds(workInfo.nextScheduleTimeMillis)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat.get(),
|
||||
|
||||
+14
-11
@@ -10,39 +10,42 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MultiSelectListPreferenceWidget(
|
||||
preference: Preference.PreferenceItem.MultiSelectListPreference,
|
||||
values: Set<String>,
|
||||
onValuesChange: (Set<String>) -> Unit,
|
||||
fun <T> MultiSelectListPreferenceWidget(
|
||||
values: Set<T>,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
icon: ImageVector?,
|
||||
entries: Map<out T, String>,
|
||||
onValuesChange: (Set<T>) -> Unit,
|
||||
) {
|
||||
var isDialogShown by remember { mutableStateOf(false) }
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = preference.title,
|
||||
subtitle = preference.subtitleProvider(values, preference.entries),
|
||||
icon = preference.icon,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
onPreferenceClick = { isDialogShown = true },
|
||||
)
|
||||
|
||||
if (isDialogShown) {
|
||||
val selected = remember {
|
||||
preference.entries.keys
|
||||
entries.keys
|
||||
.filter { values.contains(it) }
|
||||
.toMutableStateList()
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { isDialogShown = false },
|
||||
title = { Text(text = preference.title) },
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
LazyColumn {
|
||||
preference.entries.forEach { current ->
|
||||
entries.forEach { current ->
|
||||
item {
|
||||
val isSelected = selected.contains(current.key)
|
||||
LabeledCheckbox(
|
||||
|
||||
+27
-11
@@ -2,6 +2,7 @@ package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -15,6 +16,8 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
@@ -26,7 +29,7 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
fun TrackingPreferenceWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
tracker: Tracker,
|
||||
checked: Boolean,
|
||||
isLoggedIn: Boolean,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val highlighted = LocalPreferenceHighlighted.current
|
||||
@@ -39,16 +42,29 @@ fun TrackingPreferenceWidget(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(tracker)
|
||||
Text(
|
||||
text = tracker.name,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = TitleFontSize,
|
||||
)
|
||||
if (checked) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = tracker.name,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = TitleFontSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
val displayName = tracker.getDisplayUsername()
|
||||
if (isLoggedIn && displayName.isNotBlank()) {
|
||||
Text(
|
||||
text = displayName,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Done,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -18,7 +18,7 @@ import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||
@@ -29,17 +29,17 @@ private val ReaderOrientationsWithoutDefault = ReaderOrientation.entries - Reade
|
||||
@Composable
|
||||
fun OrientationSelectDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: ReaderSettingsScreenModel,
|
||||
viewModel: ReaderSettingsViewModel,
|
||||
onChange: (StringResource) -> Unit,
|
||||
) {
|
||||
val manga by screenModel.mangaFlow.collectAsState()
|
||||
val manga by viewModel.mangaFlow.collectAsState()
|
||||
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
|
||||
|
||||
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||
DialogContent(
|
||||
orientation = orientation,
|
||||
onChangeOrientation = {
|
||||
screenModel.onChangeOrientation(it)
|
||||
viewModel.onChangeOrientation(it)
|
||||
onChange(it.stringRes)
|
||||
onDismissRequest()
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import eu.kanade.domain.manga.model.readingMode
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.reader.components.ModeSelectionDialog
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||
@@ -31,17 +31,17 @@ private val ReadingModesWithoutDefault = ReadingMode.entries - ReadingMode.DEFAU
|
||||
@Composable
|
||||
fun ReadingModeSelectDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: ReaderSettingsScreenModel,
|
||||
viewModel: ReaderSettingsViewModel,
|
||||
onChange: (StringResource) -> Unit,
|
||||
) {
|
||||
val manga by screenModel.mangaFlow.collectAsState()
|
||||
val manga by viewModel.mangaFlow.collectAsState()
|
||||
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
|
||||
|
||||
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||
DialogContent(
|
||||
readingMode = readingMode,
|
||||
onChangeReadingMode = {
|
||||
screenModel.onChangeReadingMode(it)
|
||||
viewModel.onChangeReadingMode(it)
|
||||
onChange(it.stringRes)
|
||||
onDismissRequest()
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@@ -55,6 +57,7 @@ fun ReaderAppBars(
|
||||
onShare: (() -> Unit)?,
|
||||
|
||||
chapterNavigatorType: ChapterNavigatorType,
|
||||
verticalNavigatorHeight: Float,
|
||||
onNextChapter: () -> Unit,
|
||||
enabledNext: Boolean,
|
||||
onPreviousChapter: () -> Unit,
|
||||
@@ -62,6 +65,7 @@ fun ReaderAppBars(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
onPageIndexChangeFinished: () -> Unit,
|
||||
|
||||
readingMode: ReadingMode,
|
||||
onClickReadingMode: () -> Unit,
|
||||
@@ -111,16 +115,23 @@ fun ReaderAppBars(
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
|
||||
ChapterNavigator(
|
||||
type = chapterNavigatorType,
|
||||
onNextChapter = onNextChapter,
|
||||
enabledNext = enabledNext,
|
||||
onPreviousChapter = onPreviousChapter,
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
ChapterNavigator(
|
||||
modifier = Modifier.fillMaxHeight(verticalNavigatorHeight),
|
||||
type = chapterNavigatorType,
|
||||
onNextChapter = onNextChapter,
|
||||
enabledNext = enabledNext,
|
||||
onPreviousChapter = onPreviousChapter,
|
||||
enabledPrevious = enabledPrevious,
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
onPageIndexChangeFinished = onPageIndexChangeFinished,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -146,6 +157,7 @@ fun ReaderAppBars(
|
||||
currentPage = currentPage,
|
||||
totalPages = totalPages,
|
||||
onPageIndexChange = onPageIndexChange,
|
||||
onPageIndexChangeFinished = onPageIndexChangeFinished,
|
||||
)
|
||||
}
|
||||
ReaderBottomBar(
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalSlider
|
||||
import androidx.compose.material3.rememberSliderState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -71,16 +70,21 @@ fun ChapterNavigator(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onPageIndexChange: (Int) -> Unit,
|
||||
onPageIndexChangeFinished: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
val state = rememberSliderState(
|
||||
value = currentPage.toFloat(),
|
||||
steps = totalPages - 2,
|
||||
valueRange = 1f..totalPages.toFloat(),
|
||||
)
|
||||
val state = remember(totalPages) {
|
||||
SliderState(
|
||||
value = currentPage.toFloat(),
|
||||
steps = totalPages - 2,
|
||||
valueRange = 1f..totalPages.toFloat(),
|
||||
)
|
||||
}
|
||||
state.value = currentPage.toFloat()
|
||||
state.onValueChange = { onPageIndexChange(it.roundToInt() - 1) }
|
||||
state.onValueChangeFinished = onPageIndexChangeFinished
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val sliderDragged by interactionSource.collectIsDraggedAsState()
|
||||
@@ -103,7 +107,7 @@ fun ChapterNavigator(
|
||||
)
|
||||
|
||||
if (type.isHorizontal()) {
|
||||
ChapterNavigator(
|
||||
HorizontalChapterNavigator(
|
||||
isRtl = type == ChapterNavigatorType.HORIZONTAL_RTL,
|
||||
state = state,
|
||||
onNextChapter = onNextChapter,
|
||||
@@ -116,6 +120,7 @@ fun ChapterNavigator(
|
||||
mainAxisPadding = mainAxisPadding,
|
||||
backgroundColor = backgroundColor,
|
||||
buttonColor = buttonColor,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
VerticalChapterNavigator(
|
||||
@@ -130,12 +135,13 @@ fun ChapterNavigator(
|
||||
mainAxisPadding = mainAxisPadding,
|
||||
backgroundColor = backgroundColor,
|
||||
buttonColor = buttonColor,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChapterNavigator(
|
||||
fun HorizontalChapterNavigator(
|
||||
isRtl: Boolean,
|
||||
state: SliderState,
|
||||
onNextChapter: () -> Unit,
|
||||
@@ -148,13 +154,14 @@ fun ChapterNavigator(
|
||||
mainAxisPadding: Dp,
|
||||
backgroundColor: Color,
|
||||
buttonColor: IconButtonColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
|
||||
// We explicitly handle direction based on the reader viewer rather than the system direction
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = mainAxisPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -232,9 +239,10 @@ fun VerticalChapterNavigator(
|
||||
mainAxisPadding: Dp,
|
||||
backgroundColor: Color,
|
||||
buttonColor: IconButtonColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = mainAxisPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -304,6 +312,7 @@ private fun ChapterNavigatorPreview() {
|
||||
currentPage = currentPage,
|
||||
totalPages = 10,
|
||||
onPageIndexChange = { currentPage = (it + 1) },
|
||||
onPageIndexChangeFinished = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.Companion.ColorFilterMode
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
@@ -21,11 +21,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val customBrightness by screenModel.preferences.customBrightness.collectAsState()
|
||||
internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
|
||||
val customBrightness by viewModel.preferences.customBrightness.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
pref = screenModel.preferences.customBrightness,
|
||||
pref = viewModel.preferences.customBrightness,
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -35,31 +35,31 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
* 0 sets system brightness and hides the overlay.
|
||||
*/
|
||||
if (customBrightness) {
|
||||
val customBrightnessValue by screenModel.preferences.customBrightnessValue.collectAsState()
|
||||
val customBrightnessValue by viewModel.preferences.customBrightnessValue.collectAsState()
|
||||
SliderItem(
|
||||
value = customBrightnessValue,
|
||||
valueRange = -75..100,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.pref_custom_brightness),
|
||||
onChange = { screenModel.preferences.customBrightnessValue.set(it) },
|
||||
onChange = { viewModel.preferences.customBrightnessValue.set(it) },
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
}
|
||||
|
||||
val colorFilter by screenModel.preferences.colorFilter.collectAsState()
|
||||
val colorFilter by viewModel.preferences.colorFilter.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_custom_color_filter),
|
||||
pref = screenModel.preferences.colorFilter,
|
||||
pref = viewModel.preferences.colorFilter,
|
||||
)
|
||||
if (colorFilter) {
|
||||
val colorFilterValue by screenModel.preferences.colorFilterValue.collectAsState()
|
||||
val colorFilterValue by viewModel.preferences.colorFilterValue.collectAsState()
|
||||
SliderItem(
|
||||
value = colorFilterValue.red,
|
||||
valueRange = 0..255,
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_r_value),
|
||||
onChange = { newRValue ->
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
viewModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newRValue, RED_MASK, 16)
|
||||
}
|
||||
},
|
||||
@@ -71,7 +71,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_g_value),
|
||||
onChange = { newGValue ->
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
viewModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newGValue, GREEN_MASK, 8)
|
||||
}
|
||||
},
|
||||
@@ -83,7 +83,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_b_value),
|
||||
onChange = { newBValue ->
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
viewModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newBValue, BLUE_MASK, 0)
|
||||
}
|
||||
},
|
||||
@@ -95,19 +95,19 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
steps = 0,
|
||||
label = stringResource(MR.strings.color_filter_a_value),
|
||||
onChange = { newAValue ->
|
||||
screenModel.preferences.colorFilterValue.getAndSet {
|
||||
viewModel.preferences.colorFilterValue.getAndSet {
|
||||
getColorValue(it, newAValue, ALPHA_MASK, 24)
|
||||
}
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode.collectAsState()
|
||||
val colorFilterMode by viewModel.preferences.colorFilterMode.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_color_filter_mode) {
|
||||
ColorFilterMode.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = colorFilterMode == index,
|
||||
onClick = { screenModel.preferences.colorFilterMode.set(index) },
|
||||
onClick = { viewModel.preferences.colorFilterMode.set(index) },
|
||||
label = { Text(stringResource(it.first)) },
|
||||
)
|
||||
}
|
||||
@@ -116,11 +116,11 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_grayscale),
|
||||
pref = screenModel.preferences.grayscale,
|
||||
pref = viewModel.preferences.grayscale,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_inverted_colors),
|
||||
pref = screenModel.preferences.invertedColors,
|
||||
pref = viewModel.preferences.invertedColors,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
@@ -32,25 +33,25 @@ private val flashColors = listOf(
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val readerTheme by screenModel.preferences.readerTheme.collectAsState()
|
||||
internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
|
||||
val readerTheme by viewModel.preferences.readerTheme.collectAsState()
|
||||
|
||||
val flashPageState by screenModel.preferences.flashOnPageChange.collectAsState()
|
||||
val flashPageState by viewModel.preferences.flashOnPageChange.collectAsState()
|
||||
|
||||
val flashMillisPref = screenModel.preferences.flashDurationMillis
|
||||
val flashMillisPref = viewModel.preferences.flashDurationMillis
|
||||
val flashMillis by flashMillisPref.collectAsState()
|
||||
|
||||
val flashIntervalPref = screenModel.preferences.flashPageInterval
|
||||
val flashIntervalPref = viewModel.preferences.flashPageInterval
|
||||
val flashInterval by flashIntervalPref.collectAsState()
|
||||
|
||||
val flashColorPref = screenModel.preferences.flashColor
|
||||
val flashColorPref = viewModel.preferences.flashColor
|
||||
val flashColor by flashColorPref.collectAsState()
|
||||
|
||||
SettingsChipRow(MR.strings.pref_reader_theme) {
|
||||
themes.map { (labelRes, value) ->
|
||||
FilterChip(
|
||||
selected = readerTheme == value,
|
||||
onClick = { screenModel.preferences.readerTheme.set(value) },
|
||||
onClick = { viewModel.preferences.readerTheme.set(value) },
|
||||
label = { Text(stringResource(labelRes)) },
|
||||
)
|
||||
}
|
||||
@@ -58,58 +59,82 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_show_page_number),
|
||||
pref = screenModel.preferences.showPageNumber,
|
||||
pref = viewModel.preferences.showPageNumber,
|
||||
)
|
||||
|
||||
val verticalNavigatorForLongStrip by screenModel.preferences.verticalNavigatorForLongStrip.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator),
|
||||
pref = screenModel.preferences.verticalNavigatorForLongStrip,
|
||||
)
|
||||
val verticalNavigatorModes by viewModel.preferences.verticalNavigator.collectAsState()
|
||||
|
||||
SettingsChipRow(MR.strings.pref_vertical_navigator) {
|
||||
ReadingMode.entries.filter { it != ReadingMode.DEFAULT }.forEach { mode ->
|
||||
FilterChip(
|
||||
selected = verticalNavigatorModes.contains(mode),
|
||||
onClick = {
|
||||
val newModes = if (verticalNavigatorModes.contains(mode)) {
|
||||
verticalNavigatorModes - mode
|
||||
} else {
|
||||
verticalNavigatorModes + mode
|
||||
}
|
||||
viewModel.preferences.verticalNavigator.set(newModes)
|
||||
},
|
||||
label = { Text(stringResource(mode.stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (verticalNavigatorModes.isNotEmpty()) {
|
||||
val verticalNavigatorHeightPref = viewModel.preferences.verticalNavigatorHeight
|
||||
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
|
||||
|
||||
if (verticalNavigatorForLongStrip) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
|
||||
pref = screenModel.preferences.verticalNavigatorOnLeft,
|
||||
pref = viewModel.preferences.verticalNavigatorOnLeft,
|
||||
)
|
||||
|
||||
SliderItem(
|
||||
label = stringResource(MR.strings.pref_vertical_navigator_height),
|
||||
value = verticalNavigatorHeight,
|
||||
valueRange = 65..100,
|
||||
steps = 6,
|
||||
onChange = { verticalNavigatorHeightPref.set(it) },
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_fullscreen),
|
||||
pref = screenModel.preferences.fullscreen,
|
||||
pref = viewModel.preferences.fullscreen,
|
||||
)
|
||||
|
||||
val isFullscreen by screenModel.preferences.fullscreen.collectAsState()
|
||||
val isFullscreen by viewModel.preferences.fullscreen.collectAsState()
|
||||
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_cutout_short),
|
||||
pref = screenModel.preferences.drawUnderCutout,
|
||||
pref = viewModel.preferences.drawUnderCutout,
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_keep_screen_on),
|
||||
pref = screenModel.preferences.keepScreenOn,
|
||||
pref = viewModel.preferences.keepScreenOn,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_read_with_long_tap),
|
||||
pref = screenModel.preferences.readWithLongTap,
|
||||
pref = viewModel.preferences.readWithLongTap,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
pref = screenModel.preferences.alwaysShowChapterTransition,
|
||||
pref = viewModel.preferences.alwaysShowChapterTransition,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_transitions),
|
||||
pref = screenModel.preferences.pageTransitions,
|
||||
pref = viewModel.preferences.pageTransitions,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_flash_page),
|
||||
pref = screenModel.preferences.flashOnPageChange,
|
||||
pref = viewModel.preferences.flashOnPageChange,
|
||||
)
|
||||
if (flashPageState) {
|
||||
SliderItem(
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@@ -23,7 +23,7 @@ fun ReaderSettingsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onShowMenus: () -> Unit,
|
||||
onHideMenus: () -> Unit,
|
||||
screenModel: ReaderSettingsScreenModel,
|
||||
viewModel: ReaderSettingsViewModel,
|
||||
) {
|
||||
val tabTitles = listOf(
|
||||
stringResource(MR.strings.pref_category_reading_mode),
|
||||
@@ -60,9 +60,9 @@ fun ReaderSettingsDialog(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ReadingModePage(screenModel)
|
||||
1 -> GeneralPage(screenModel)
|
||||
2 -> ColorFilterPage(screenModel)
|
||||
0 -> ReadingModePage(viewModel)
|
||||
1 -> GeneralPage(viewModel)
|
||||
2 -> ColorFilterPage(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.kanade.domain.manga.model.readerOrientation
|
||||
import eu.kanade.domain.manga.model.readingMode
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -25,16 +25,16 @@ import tachiyomi.presentation.core.util.collectAsState
|
||||
import java.text.NumberFormat
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
|
||||
internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
|
||||
HeadingItem(MR.strings.pref_category_for_this_series)
|
||||
val manga by screenModel.mangaFlow.collectAsState()
|
||||
val manga by viewModel.mangaFlow.collectAsState()
|
||||
|
||||
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
|
||||
SettingsChipRow(MR.strings.pref_category_reading_mode) {
|
||||
ReadingMode.entries.map {
|
||||
FilterChip(
|
||||
selected = it == readingMode,
|
||||
onClick = { screenModel.onChangeReadingMode(it) },
|
||||
onClick = { viewModel.onChangeReadingMode(it) },
|
||||
label = { Text(stringResource(it.stringRes)) },
|
||||
)
|
||||
}
|
||||
@@ -45,50 +45,50 @@ internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel)
|
||||
ReaderOrientation.entries.map {
|
||||
FilterChip(
|
||||
selected = it == orientation,
|
||||
onClick = { screenModel.onChangeOrientation(it) },
|
||||
onClick = { viewModel.onChangeOrientation(it) },
|
||||
label = { Text(stringResource(it.stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val viewer by screenModel.viewerFlow.collectAsState()
|
||||
val viewer by viewModel.viewerFlow.collectAsState()
|
||||
if (viewer is WebtoonViewer) {
|
||||
WebtoonViewerSettings(screenModel)
|
||||
WebtoonViewerSettings(viewModel)
|
||||
} else {
|
||||
PagerViewerSettings(screenModel)
|
||||
PagerViewerSettings(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
|
||||
private fun ColumnScope.PagerViewerSettings(viewModel: ReaderSettingsViewModel) {
|
||||
HeadingItem(MR.strings.pager_viewer)
|
||||
|
||||
val navigationModePager by screenModel.preferences.navigationModePager.collectAsState()
|
||||
val pagerNavInverted by screenModel.preferences.pagerNavInverted.collectAsState()
|
||||
val navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
|
||||
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModePager,
|
||||
onSelect = screenModel.preferences.navigationModePager::set,
|
||||
onSelect = viewModel.preferences.navigationModePager::set,
|
||||
invertMode = pagerNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.pagerNavInverted::set,
|
||||
onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
|
||||
)
|
||||
|
||||
val imageScaleType by screenModel.preferences.imageScaleType.collectAsState()
|
||||
val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_image_scale_type) {
|
||||
ReaderPreferences.ImageScaleType.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = imageScaleType == index + 1,
|
||||
onClick = { screenModel.preferences.imageScaleType.set(index + 1) },
|
||||
onClick = { viewModel.preferences.imageScaleType.set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val zoomStart by screenModel.preferences.zoomStart.collectAsState()
|
||||
val zoomStart by viewModel.preferences.zoomStart.collectAsState()
|
||||
SettingsChipRow(MR.strings.pref_zoom_start) {
|
||||
ReaderPreferences.ZoomStart.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = zoomStart == index + 1,
|
||||
onClick = { screenModel.preferences.zoomStart.set(index + 1) },
|
||||
onClick = { viewModel.preferences.zoomStart.set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
@@ -96,111 +96,111 @@ private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenMod
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_crop_borders),
|
||||
pref = screenModel.preferences.cropBorders,
|
||||
pref = viewModel.preferences.cropBorders,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_landscape_zoom),
|
||||
pref = screenModel.preferences.landscapeZoom,
|
||||
pref = viewModel.preferences.landscapeZoom,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_navigate_pan),
|
||||
pref = screenModel.preferences.navigateToPan,
|
||||
pref = viewModel.preferences.navigateToPan,
|
||||
)
|
||||
|
||||
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged.collectAsState()
|
||||
val dualPageSplitPaged by viewModel.preferences.dualPageSplitPaged.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_split),
|
||||
pref = screenModel.preferences.dualPageSplitPaged,
|
||||
pref = viewModel.preferences.dualPageSplitPaged,
|
||||
)
|
||||
|
||||
if (dualPageSplitPaged) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_invert),
|
||||
pref = screenModel.preferences.dualPageInvertPaged,
|
||||
pref = viewModel.preferences.dualPageInvertPaged,
|
||||
)
|
||||
}
|
||||
|
||||
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit.collectAsState()
|
||||
val dualPageRotateToFit by viewModel.preferences.dualPageRotateToFit.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate),
|
||||
pref = screenModel.preferences.dualPageRotateToFit,
|
||||
pref = viewModel.preferences.dualPageRotateToFit,
|
||||
)
|
||||
|
||||
if (dualPageRotateToFit) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvert,
|
||||
pref = viewModel.preferences.dualPageRotateToFitInvert,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) {
|
||||
private fun ColumnScope.WebtoonViewerSettings(viewModel: ReaderSettingsViewModel) {
|
||||
val numberFormat = remember { NumberFormat.getPercentInstance() }
|
||||
|
||||
HeadingItem(MR.strings.webtoon_viewer)
|
||||
|
||||
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon.collectAsState()
|
||||
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted.collectAsState()
|
||||
val navigationModeWebtoon by viewModel.preferences.navigationModeWebtoon.collectAsState()
|
||||
val webtoonNavInverted by viewModel.preferences.webtoonNavInverted.collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModeWebtoon,
|
||||
onSelect = screenModel.preferences.navigationModeWebtoon::set,
|
||||
onSelect = viewModel.preferences.navigationModeWebtoon::set,
|
||||
invertMode = webtoonNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.webtoonNavInverted::set,
|
||||
onSelectInvertMode = viewModel.preferences.webtoonNavInverted::set,
|
||||
)
|
||||
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding.collectAsState()
|
||||
val webtoonSidePadding by viewModel.preferences.webtoonSidePadding.collectAsState()
|
||||
SliderItem(
|
||||
value = webtoonSidePadding,
|
||||
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
|
||||
label = stringResource(MR.strings.pref_webtoon_side_padding),
|
||||
valueString = numberFormat.format(webtoonSidePadding / 100f),
|
||||
onChange = {
|
||||
screenModel.preferences.webtoonSidePadding.set(it)
|
||||
viewModel.preferences.webtoonSidePadding.set(it)
|
||||
},
|
||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_crop_borders),
|
||||
pref = screenModel.preferences.cropBordersWebtoon,
|
||||
pref = viewModel.preferences.cropBordersWebtoon,
|
||||
)
|
||||
|
||||
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon.collectAsState()
|
||||
val dualPageSplitWebtoon by viewModel.preferences.dualPageSplitWebtoon.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_split),
|
||||
pref = screenModel.preferences.dualPageSplitWebtoon,
|
||||
pref = viewModel.preferences.dualPageSplitWebtoon,
|
||||
)
|
||||
|
||||
if (dualPageSplitWebtoon) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_dual_page_invert),
|
||||
pref = screenModel.preferences.dualPageInvertWebtoon,
|
||||
pref = viewModel.preferences.dualPageInvertWebtoon,
|
||||
)
|
||||
}
|
||||
|
||||
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
|
||||
val dualPageRotateToFitWebtoon by viewModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate),
|
||||
pref = screenModel.preferences.dualPageRotateToFitWebtoon,
|
||||
pref = viewModel.preferences.dualPageRotateToFitWebtoon,
|
||||
)
|
||||
|
||||
if (dualPageRotateToFitWebtoon) {
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_page_rotate_invert),
|
||||
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon,
|
||||
pref = viewModel.preferences.dualPageRotateToFitInvertWebtoon,
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled,
|
||||
pref = viewModel.preferences.webtoonDoubleTapZoomEnabled,
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut,
|
||||
pref = viewModel.preferences.webtoonDisableZoomOut,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TakoColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TealTurqoiseColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TidalWaveColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.TokyoNightColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.YinYangColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.YotsubaColorScheme
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -90,6 +91,7 @@ private fun getThemeColorScheme(
|
||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||
AppTheme.CATPPUCCIN to CatppuccinColorScheme,
|
||||
AppTheme.TOKYONIGHT to TokyoNightColorScheme,
|
||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||
AppTheme.LAVENDER to LavenderColorScheme,
|
||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package eu.kanade.presentation.theme.colorscheme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Colors for Tokyo Night Moon/Day
|
||||
*
|
||||
* https://github.com/folke/tokyonight.nvim
|
||||
*/
|
||||
internal object TokyoNightColorScheme : BaseColorScheme() {
|
||||
|
||||
override val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFF82AAFF),
|
||||
onPrimary = Color(0xFF1B1D2B),
|
||||
primaryContainer = Color(0xFF444A73),
|
||||
onPrimaryContainer = Color(0xFF9AB8FF),
|
||||
secondary = Color(0xFF86E1FC),
|
||||
onSecondary = Color(0xFF1B1D2B),
|
||||
secondaryContainer = Color(0xFF444A73),
|
||||
onSecondaryContainer = Color(0xFFB2EBFF),
|
||||
tertiary = Color(0xFFC099FF),
|
||||
onTertiary = Color(0xFF1B1D2B),
|
||||
tertiaryContainer = Color(0xFF444A73),
|
||||
onTertiaryContainer = Color(0xFFCAABFF),
|
||||
error = Color(0xFFFF757F),
|
||||
onError = Color(0xFF1B1D2B),
|
||||
errorContainer = Color(0xFFC53B53),
|
||||
onErrorContainer = Color(0xFFFF8D94),
|
||||
background = Color(0xFF222436),
|
||||
onBackground = Color(0xFFC8D3F5),
|
||||
surface = Color(0xFF222436),
|
||||
onSurface = Color(0xFFC8D3F5),
|
||||
surfaceVariant = Color(0xFF444A73),
|
||||
onSurfaceVariant = Color(0xFF828BB8),
|
||||
outline = Color(0xFF828BB8),
|
||||
outlineVariant = Color(0xFF444A73),
|
||||
scrim = Color(0xFF000000),
|
||||
inverseSurface = Color(0xFFC8D3F5),
|
||||
inverseOnSurface = Color(0xFF222436),
|
||||
inversePrimary = Color(0xFF3D5C9E),
|
||||
surfaceContainerLowest = Color(0xFF1E202F),
|
||||
surfaceDim = Color(0xFF222436),
|
||||
surfaceContainerLow = Color(0xFF292C44),
|
||||
surfaceContainer = Color(0xFF2D304B),
|
||||
surfaceContainerHigh = Color(0xFF363B5D),
|
||||
surfaceContainerHighest = Color(0xFF3F466F),
|
||||
surfaceBright = Color(0xFF434A77),
|
||||
)
|
||||
|
||||
override val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF2E7DE9),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFF92A6D5),
|
||||
onPrimaryContainer = Color(0xFF15386A),
|
||||
inversePrimary = Color(0xFF7890DD),
|
||||
secondary = Color(0xFF007197),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFFA9C6D3),
|
||||
onSecondaryContainer = Color(0xFF006A83),
|
||||
tertiary = Color(0xFF9854F1),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFFCFBFEA),
|
||||
onTertiaryContainer = Color(0xFF7847BD),
|
||||
background = Color(0xFFE1E2E7),
|
||||
onBackground = Color(0xFF3760BF),
|
||||
surface = Color(0xFFE1E2E7),
|
||||
onSurface = Color(0xFF3760BF),
|
||||
surfaceVariant = Color(0xFFC4C8DA),
|
||||
onSurfaceVariant = Color(0xFF6172B0),
|
||||
surfaceTint = Color(0xFF2E7DE9),
|
||||
inverseSurface = Color(0xFF3760BF),
|
||||
inverseOnSurface = Color(0xFFE1E2E7),
|
||||
outline = Color(0xFF68709A),
|
||||
outlineVariant = Color(0xFFA8AECB),
|
||||
error = Color(0xFFF52A65),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFFE6B4C7),
|
||||
onErrorContainer = Color(0xFF6E132D),
|
||||
surfaceDim = Color(0xFF9195B1),
|
||||
surfaceBright = Color(0xFFE1E2E7),
|
||||
surfaceContainerLowest = Color(0xFFE4E5E9),
|
||||
surfaceContainerLow = Color(0xFFD0D5E3),
|
||||
surfaceContainer = Color(0xFFC1C9DF),
|
||||
surfaceContainerHigh = Color(0xFFC4C8DA),
|
||||
surfaceContainerHighest = Color(0xFFA1A6C5),
|
||||
)
|
||||
}
|
||||
@@ -56,8 +56,10 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.lang.toJavaLocalDate
|
||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -110,11 +112,13 @@ fun TrackInfoDialogHome(
|
||||
.takeIf { supportsScoring && item.track.score != 0.0 },
|
||||
onScoreClick = { onScoreClick(item) }
|
||||
.takeIf { supportsScoring },
|
||||
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) }
|
||||
startDate = remember(item.track.startDate) {
|
||||
dateFormat.format(item.track.startDate.toLocalDate().toJavaLocalDate())
|
||||
}
|
||||
.takeIf { supportsReadingDates && item.track.startDate != 0L },
|
||||
onStartDateClick = { onStartDateEdit(item) } // TODO
|
||||
.takeIf { supportsReadingDates },
|
||||
endDate = dateFormat.format(item.track.finishDate.toLocalDate())
|
||||
endDate = dateFormat.format(item.track.finishDate.toJavaLocalDate())
|
||||
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
|
||||
onEndDateClick = { onEndDateEdit(item) }
|
||||
.takeIf { supportsReadingDates },
|
||||
|
||||
@@ -6,11 +6,12 @@ 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
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
|
||||
private val fullPageWithSecondSelected = @Composable {
|
||||
@@ -93,7 +94,7 @@ 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 = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
|
||||
it.start_date = formatter.format(Date.from(Clock.System.now().minus((1L..365).random().days).toJavaInstant()))
|
||||
it.summary = lorem((0..40).random()).joinToString()
|
||||
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
|
||||
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
|
||||
|
||||
@@ -14,12 +14,19 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsViewModel
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -27,66 +34,71 @@ import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@Composable
|
||||
fun UpdatesFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
viewModel: UpdatesSettingsViewModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = listOf(
|
||||
stringResource(MR.strings.action_filter),
|
||||
stringResource(MR.strings.categories),
|
||||
),
|
||||
) {
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterSheet(screenModel = screenModel)
|
||||
when (page) {
|
||||
0 -> FilterSheet(viewModel = viewModel)
|
||||
1 -> CategoryFilterSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.FilterSheet(
|
||||
screenModel: UpdatesSettingsScreenModel,
|
||||
viewModel: UpdatesSettingsViewModel,
|
||||
) {
|
||||
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded.collectAsState()
|
||||
val filterDownloaded by viewModel.updatesPreferences.filterDownloaded.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_downloaded),
|
||||
state = filterDownloaded,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
|
||||
)
|
||||
|
||||
val filterUnread by screenModel.updatesPreferences.filterUnread.collectAsState()
|
||||
val filterUnread by viewModel.updatesPreferences.filterUnread.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_unread),
|
||||
state = filterUnread,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterUnread) },
|
||||
)
|
||||
|
||||
val filterStarted by screenModel.updatesPreferences.filterStarted.collectAsState()
|
||||
val filterStarted by viewModel.updatesPreferences.filterStarted.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.label_started),
|
||||
state = filterStarted,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterStarted) },
|
||||
)
|
||||
|
||||
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked.collectAsState()
|
||||
val filterBookmarked by viewModel.updatesPreferences.filterBookmarked.collectAsState()
|
||||
TriStateItem(
|
||||
label = stringResource(MR.strings.action_filter_bookmarked),
|
||||
state = filterBookmarked,
|
||||
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
|
||||
|
||||
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators.collectAsState()
|
||||
val filterExcludedScanlators by viewModel.updatesPreferences.filterExcludedScanlators.collectAsState()
|
||||
|
||||
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it }
|
||||
fun toggleScanlatorFilter() = viewModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -108,3 +120,55 @@ private fun ColumnScope.FilterSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.CategoryFilterSheet(
|
||||
viewModel: UpdatesSettingsViewModel,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.pref_filter_update_categories_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.extraSmall))
|
||||
|
||||
val allCategories by viewModel.getCategories.subscribe().collectAsState(initial = emptyList())
|
||||
|
||||
if (allCategories.isEmpty()) {
|
||||
// since it includes the system category, this should only happen when loading is required
|
||||
LoadingScreen(modifier = Modifier.padding(16.dp))
|
||||
return
|
||||
}
|
||||
|
||||
val excluded by viewModel.updatesPreferences.filterExcludedCategories.collectAsState()
|
||||
val included by viewModel.updatesPreferences.filterIncludedCategories.collectAsState()
|
||||
|
||||
val selected = remember {
|
||||
allCategories.map { category ->
|
||||
when (category.id) {
|
||||
in included -> TriState.ENABLED_IS
|
||||
in excluded -> TriState.ENABLED_NOT
|
||||
else -> TriState.DISABLED
|
||||
}
|
||||
}.toMutableStateList()
|
||||
}
|
||||
|
||||
Column {
|
||||
allCategories.fastForEachIndexed { idx, category ->
|
||||
val state = selected[idx]
|
||||
TriStateItem(
|
||||
label = category.visualName,
|
||||
state = state,
|
||||
onClick = {
|
||||
selected[idx] = state.next()
|
||||
viewModel.cycleCategory(category)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,10 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.manga.components.MangaBottomActionMenu
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
@@ -40,12 +41,11 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun UpdateScreen(
|
||||
state: UpdatesScreenModel.State,
|
||||
state: UpdatesViewModel.State,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
lastUpdated: Long,
|
||||
onClickCover: (UpdatesItem) -> Unit,
|
||||
|
||||
@@ -37,7 +37,6 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
|
||||
@@ -92,14 +91,14 @@ internal fun LazyListScope.updatesUiItems(
|
||||
when (item) {
|
||||
is UpdatesUiModel.Header -> {
|
||||
ListGroupHeader(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
text = relativeDateText(item.date),
|
||||
)
|
||||
}
|
||||
is UpdatesUiModel.Item -> {
|
||||
val updatesItem = item.item
|
||||
UpdatesUiItem(
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
modifier = Modifier.animateItem(),
|
||||
update = updatesItem.update,
|
||||
selected = updatesItem.selected,
|
||||
readProgress = updatesItem.update.lastPageRead
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
// https://issuetracker.google.com/352584409
|
||||
context(itemScope: LazyItemScope)
|
||||
fun Modifier.animateItemFastScroll() = with(itemScope) {
|
||||
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
@@ -8,20 +8,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import cafe.adriel.voyager.core.stack.StackEvent
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.plus
|
||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
|
||||
@@ -39,18 +31,6 @@ abstract class Screen : Screen {
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of ScreenModel.coroutineScope except with the IO dispatcher instead of the
|
||||
* main dispatcher.
|
||||
*/
|
||||
val ScreenModel.ioCoroutineScope: CoroutineScope
|
||||
get() = ScreenModelStore.getOrPutDependency(
|
||||
screenModel = this,
|
||||
name = "ScreenModelIoCoroutineScope",
|
||||
factory = { key -> CoroutineScope(Dispatchers.IO + SupervisorJob()) + CoroutineName(key) },
|
||||
onDispose = { scope -> scope.cancel() },
|
||||
)
|
||||
|
||||
interface AssistContentScreen {
|
||||
fun onProvideAssistUrl(): String?
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.time.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@@ -29,7 +29,7 @@ fun Duration.toDurationString(context: Context, fallback: String): String {
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun relativeTimeSpanString(epochMillis: Long): String {
|
||||
val now = Instant.now().toEpochMilli()
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
return when {
|
||||
epochMillis <= 0L -> stringResource(MR.strings.relative_time_span_never)
|
||||
now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource(
|
||||
|
||||
@@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getHtml
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.setUserAgent
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
@@ -72,6 +73,7 @@ fun WebViewScreenContent(
|
||||
onNavigateUp: () -> Unit,
|
||||
initialTitle: String?,
|
||||
url: String,
|
||||
defaultUserAgentProvider: () -> String,
|
||||
onShare: (String) -> Unit,
|
||||
onOpenInBrowser: (String) -> Unit,
|
||||
onClearCookies: (String) -> Unit,
|
||||
@@ -340,9 +342,7 @@ fun WebViewScreenContent(
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
|
||||
headers["user-agent"]?.let {
|
||||
webView.settings.userAgentString = it
|
||||
}
|
||||
webView.setUserAgent(headers["user-agent"] ?: defaultUserAgentProvider())
|
||||
},
|
||||
onDispose = { webView ->
|
||||
val window = windowStack.items.find { it.webView == webView }
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
@@ -231,7 +230,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
override fun getPackageName(): String {
|
||||
try {
|
||||
// Override the value passed as X-Requested-With in WebView requests
|
||||
val stackTrace = Looper.getMainLooper().thread.stackTrace
|
||||
val stackTrace = Thread.currentThread().stackTrace
|
||||
val isChromiumCall = stackTrace.any { trace ->
|
||||
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
|
||||
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
|
||||
|
||||
@@ -18,7 +18,7 @@ import tachiyomi.core.common.storage.displayablePath
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class BackupNotifier(private val context: Context) {
|
||||
|
||||
@@ -149,10 +149,8 @@ class BackupNotifier(private val context: Context) {
|
||||
|
||||
val timeString = context.stringResource(
|
||||
MR.strings.restore_duration,
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
),
|
||||
time.milliseconds.inWholeMinutes,
|
||||
time.milliseconds.inWholeSeconds - (time.milliseconds.inWholeMinutes * 60),
|
||||
)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
|
||||
@@ -33,9 +33,9 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.FileOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.time.Clock
|
||||
|
||||
class BackupCreator(
|
||||
private val context: Context,
|
||||
@@ -108,7 +108,7 @@ class BackupCreator(
|
||||
BackupFileValidator(context).validate(fileUri)
|
||||
|
||||
if (isAutoBackup) {
|
||||
backupPreferences.lastAutoBackupTimestamp.set(Instant.now().toEpochMilli())
|
||||
backupPreferences.lastAutoBackupTimestamp.set(Clock.System.now().toEpochMilliseconds())
|
||||
}
|
||||
|
||||
return fileUri.toString()
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup.models
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import mihon.core.common.extensions.EMPTY
|
||||
import mihon.core.common.extensions.JsonObjectEmptyBytes
|
||||
import tachiyomi.data.MemoColumnAdapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
|
||||
@@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import mihon.core.common.extensions.EMPTY
|
||||
import mihon.core.common.extensions.JsonObjectEmptyBytes
|
||||
import tachiyomi.data.MemoColumnAdapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@@ -13,12 +13,15 @@ import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.data.Database
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -59,6 +62,15 @@ class BackupRestorer(
|
||||
|
||||
restoreFromFile(uri, options)
|
||||
|
||||
// Invalidate download cache to ensure UI reflects any restored downloads
|
||||
if (options.libraryEntries) {
|
||||
try {
|
||||
Injekt.get<DownloadCache>().invalidateCache()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to invalidate download cache after restore" }
|
||||
}
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
@@ -136,13 +148,30 @@ class BackupRestorer(
|
||||
mangaRestorer.sortByNew(backupMangas)
|
||||
.chunked(100)
|
||||
.forEach { chunk ->
|
||||
database.transaction {
|
||||
val restoredAsBatch = try {
|
||||
database.transaction {
|
||||
chunk.forEach {
|
||||
ensureActive()
|
||||
mangaRestorer.restore(it, backupCategories)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
ensureActive()
|
||||
logcat(LogPriority.WARN, e) { "Batch restore failed, retrying entry by entry" }
|
||||
false
|
||||
}
|
||||
|
||||
if (restoredAsBatch) {
|
||||
restoreProgress.addAndFetch(chunk.size)
|
||||
} else {
|
||||
chunk.forEach {
|
||||
ensureActive()
|
||||
|
||||
try {
|
||||
mangaRestorer.restore(it, backupCategories)
|
||||
} catch (e: Exception) {
|
||||
ensureActive()
|
||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
@@ -150,6 +179,7 @@ class BackupRestorer(
|
||||
restoreProgress.incrementAndFetch()
|
||||
}
|
||||
}
|
||||
|
||||
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-14
@@ -9,9 +9,10 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.data.Database
|
||||
import tachiyomi.data.MemoColumnAdapter
|
||||
import tachiyomi.data.MemoColumnAdapter.encode
|
||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
@@ -24,9 +25,9 @@ import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Clock
|
||||
|
||||
class MangaRestorer(
|
||||
private val database: Database = Injekt.get(),
|
||||
@@ -39,13 +40,9 @@ class MangaRestorer(
|
||||
fetchInterval: FetchInterval = Injekt.get(),
|
||||
) {
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
init {
|
||||
now = ZonedDateTime.now()
|
||||
currentFetchWindow = fetchInterval.getWindow(now)
|
||||
}
|
||||
private val timeZone = TimeZone.currentSystemDefault()
|
||||
private val now = Clock.System.now().toLocalDateTime(timeZone)
|
||||
private val currentFetchWindow = fetchInterval.getWindow(now.date, timeZone)
|
||||
|
||||
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
|
||||
val urlsBySource = database.mangasQueries
|
||||
@@ -285,9 +282,9 @@ class MangaRestorer(
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreChapters(manga, chapters)
|
||||
restoreTracking(manga, tracks)
|
||||
restoreHistory(history)
|
||||
restoreHistory(manga, history)
|
||||
restoreExcludedScanlators(manga, excludedScanlators)
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||
updateManga.awaitUpdateFetchInterval(manga, timeZone, now, currentFetchWindow)
|
||||
return manga
|
||||
}
|
||||
|
||||
@@ -325,16 +322,16 @@ class MangaRestorer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
||||
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
|
||||
val toUpdate = backupHistory.mapNotNull { history ->
|
||||
val dbHistory = database.historyQueries
|
||||
.getHistoryByChapterUrl(history.url)
|
||||
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
|
||||
.awaitAsOneOrNull()
|
||||
val item = history.getHistoryImpl()
|
||||
|
||||
if (dbHistory == null) {
|
||||
val chapter = database.chaptersQueries
|
||||
.getChapterByUrl(history.url)
|
||||
.getChapterByUrlAndMangaId(history.url, manga.id)
|
||||
.awaitAsOneOrNull()
|
||||
return@mapNotNull if (chapter == null) {
|
||||
// Chapter doesn't exist; skip
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -42,7 +43,6 @@ import mihon.core.archive.ZipWriter
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNow
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
@@ -362,11 +362,6 @@ class Downloader(
|
||||
reIndexedPages
|
||||
}
|
||||
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.extension == "tmp" }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.status = Download.State.DOWNLOADING
|
||||
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
@@ -441,14 +436,10 @@ class Downloader(
|
||||
|
||||
val digitCount = (download.pages?.size ?: 0).toString().length.coerceAtLeast(3)
|
||||
val filename = "%0${digitCount}d".format(Locale.ENGLISH, page.number)
|
||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||
|
||||
// Delete temp file if it exists
|
||||
tmpFile?.delete()
|
||||
|
||||
// Try to find the image file
|
||||
val imageFile = tmpDir.listFiles()?.firstOrNull {
|
||||
it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001")
|
||||
isDownloadedPageImage(it.name ?: return@firstOrNull false, filename)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -488,15 +479,24 @@ class Downloader(
|
||||
page.status = Page.State.DownloadImage
|
||||
page.progress = 0
|
||||
return flow {
|
||||
val response = source.getImage(page)
|
||||
val file = tmpDir.createFile("$filename.tmp")!!
|
||||
val file = tmpDir.findFile("$filename.tmp")
|
||||
?: tmpDir.createFile("$filename.tmp")!!
|
||||
|
||||
try {
|
||||
response.body.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
source.getImage(page, file.length()).use {
|
||||
it.body.source().saveTo(
|
||||
// If the server supports partial downloads (HTTP 206),
|
||||
// append to the existing file.
|
||||
// Otherwise, start from scratch and overwrite the file.
|
||||
stream = file.openOutputStream(it.code == 206),
|
||||
)
|
||||
val extension = getImageExtension(it, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
if (e.code == 416) {
|
||||
file.delete()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
emit(file)
|
||||
@@ -521,6 +521,8 @@ class Downloader(
|
||||
* @param filename the filename of the image.
|
||||
*/
|
||||
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile {
|
||||
// Delete temp file if it exists
|
||||
tmpDir.findFile("$filename.tmp")?.delete()
|
||||
val tmpFile = tmpDir.createFile("$filename.tmp")!!
|
||||
cacheFile.inputStream().use { input ->
|
||||
tmpFile.openOutputStream().use { output ->
|
||||
@@ -594,6 +596,18 @@ class Downloader(
|
||||
return downloadedImagesCount == downloadPageCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file name matches a downloaded page image.
|
||||
*
|
||||
* @param fileName Name of the file to check
|
||||
* @param pagePrefix Expected page prefix (e.g., "001")
|
||||
*/
|
||||
private fun isDownloadedPageImage(fileName: String, pagePrefix: String): Boolean =
|
||||
!fileName.endsWith(".tmp") && (
|
||||
fileName.startsWith("$pagePrefix.") ||
|
||||
fileName.startsWith("${pagePrefix}__001.")
|
||||
)
|
||||
|
||||
/**
|
||||
* Archive the chapter pages as a CBZ.
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,8 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import mihon.domain.source.interactor.UpdateMangaFromRemote
|
||||
@@ -64,14 +66,13 @@ import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.AtomicInt
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.concurrent.atomics.incrementAndFetch
|
||||
import kotlin.time.Clock
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
@@ -108,7 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
libraryPreferences.lastUpdatedTimestamp.set(Instant.now().toEpochMilli())
|
||||
libraryPreferences.lastUpdatedTimestamp.set(Clock.System.now().toEpochMilliseconds())
|
||||
|
||||
val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
|
||||
addMangaToQueue(categoryId)
|
||||
@@ -167,7 +168,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions.get()
|
||||
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now())
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(
|
||||
Clock.System.now().toLocalDateTime(timeZone).date,
|
||||
timeZone,
|
||||
)
|
||||
|
||||
mangaToUpdate = listToUpdate
|
||||
.filter {
|
||||
@@ -234,7 +239,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val fetchWindow = fetchInterval.getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }.values
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
@@ -72,10 +71,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(context)
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||
// Start downloading app update
|
||||
ACTION_START_APP_UPDATE -> startDownloadAppUpdate(context, intent)
|
||||
// Cancel downloading app update
|
||||
ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context)
|
||||
// Open reader activity
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(
|
||||
@@ -178,15 +173,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
LibraryUpdateJob.stop(context)
|
||||
}
|
||||
|
||||
private fun startDownloadAppUpdate(context: Context, intent: Intent) {
|
||||
val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return
|
||||
AppUpdateDownloadJob.start(context, url)
|
||||
}
|
||||
|
||||
private fun cancelDownloadAppUpdate(context: Context) {
|
||||
AppUpdateDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to mark manga chapters as read
|
||||
*
|
||||
@@ -241,9 +227,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE"
|
||||
private const val ACTION_CANCEL_APP_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_APP_UPDATE_DOWNLOAD"
|
||||
|
||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
|
||||
@@ -524,45 +507,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts the [AppUpdateDownloadJob] to download an app update.
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun downloadAppUpdatePendingBroadcast(
|
||||
context: Context,
|
||||
url: String,
|
||||
title: String? = null,
|
||||
): PendingIntent {
|
||||
return Intent(context, NotificationReceiver::class.java).run {
|
||||
action = ACTION_START_APP_UPDATE
|
||||
putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url)
|
||||
title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) }
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
this,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that opens the extensions controller.
|
||||
*
|
||||
|
||||
@@ -70,8 +70,6 @@ object Notifications {
|
||||
private const val GROUP_APK_UPDATES = "group_apk_updates"
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import kotlin.time.Clock
|
||||
|
||||
class ImageSaver(
|
||||
val context: Context,
|
||||
@@ -85,7 +85,7 @@ class ImageSaver(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH to relativePath,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME to if (isMimeTypeSupported) image.name else filename,
|
||||
MediaStore.MediaColumns.MIME_TYPE to type.mime,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED to Instant.now().epochSecond,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED to Clock.System.now().epochSeconds,
|
||||
)
|
||||
|
||||
val picture = findUriOrDefault(relativePath, filename) {
|
||||
|
||||
@@ -68,6 +68,10 @@ abstract class BaseTracker(
|
||||
|
||||
override fun getUsername() = trackPreferences.trackUsername(this).get()
|
||||
|
||||
override fun getDisplayUsername(): String = trackPreferences.trackDisplayUsername(this).get()
|
||||
|
||||
override fun saveDisplayUsername(displayName: String) = trackPreferences.trackDisplayUsername(this).set(displayName)
|
||||
|
||||
override fun getPassword() = trackPreferences.trackPassword(this).get()
|
||||
|
||||
override fun saveCredentials(username: String, password: String) {
|
||||
|
||||
@@ -65,6 +65,10 @@ interface Tracker {
|
||||
|
||||
fun getPassword(): String
|
||||
|
||||
fun getDisplayUsername(): String
|
||||
|
||||
fun saveDisplayUsername(displayName: String)
|
||||
|
||||
fun saveCredentials(username: String, password: String)
|
||||
|
||||
// TODO: move this to an interactor, and update all trackers based on common data
|
||||
|
||||
@@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||
import eu.kanade.tachiyomi.data.track.hikka.Hikka
|
||||
import eu.kanade.tachiyomi.data.track.kavita.Kavita
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||
import eu.kanade.tachiyomi.data.track.mangabaka.MangaBaka
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
@@ -17,6 +19,7 @@ class TrackerManager {
|
||||
const val ANILIST = 2L
|
||||
const val KITSU = 3L
|
||||
const val KAVITA = 8L
|
||||
const val MANGABAKA = 11L
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(1L)
|
||||
@@ -28,8 +31,22 @@ class TrackerManager {
|
||||
val mangaUpdates = MangaUpdates(7L)
|
||||
val kavita = Kavita(KAVITA)
|
||||
val suwayomi = Suwayomi(9L)
|
||||
val hikka = Hikka(10L)
|
||||
val mangaBaka = MangaBaka(MANGABAKA)
|
||||
|
||||
val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
|
||||
val trackers = listOf(
|
||||
myAnimeList,
|
||||
aniList,
|
||||
kitsu,
|
||||
shikimori,
|
||||
bangumi,
|
||||
komga,
|
||||
mangaUpdates,
|
||||
kavita,
|
||||
suwayomi,
|
||||
hikka,
|
||||
mangaBaka,
|
||||
)
|
||||
|
||||
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
|
||||
|
||||
|
||||
@@ -213,9 +213,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
try {
|
||||
val oauth = api.createOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
val (username, scoreType) = api.getCurrentUser()
|
||||
scorePreference.set(scoreType)
|
||||
saveCredentials(username.toString(), oauth.accessToken)
|
||||
val currentUser = api.getCurrentUser()
|
||||
scorePreference.set(currentUser.mediaListOptions.scoreFormat)
|
||||
saveDisplayUsername(currentUser.name)
|
||||
saveCredentials(currentUser.id.toString(), oauth.accessToken)
|
||||
} catch (e: Throwable) {
|
||||
logout()
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserViewerData
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -24,10 +28,8 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Instant
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
@@ -285,12 +287,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||
suspend fun getCurrentUser(): ALUserViewerData {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
|name
|
||||
|mediaListOptions {
|
||||
|scoreFormat
|
||||
|}
|
||||
@@ -310,10 +313,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
)
|
||||
.awaitSuccess()
|
||||
.parseAs<ALCurrentUserResult>()
|
||||
.let {
|
||||
val viewer = it.data.viewer
|
||||
Pair(viewer.id, viewer.mediaListOptions.scoreFormat)
|
||||
}
|
||||
.data.viewer
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,11 +327,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
}
|
||||
}
|
||||
|
||||
val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault())
|
||||
val dateTime = Instant.fromEpochMilliseconds(dateValue).toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
return buildJsonObject {
|
||||
put("year", dateTime.year)
|
||||
put("month", dateTime.monthValue)
|
||||
put("day", dateTime.dayOfMonth)
|
||||
put("month", dateTime.month.number)
|
||||
put("day", dateTime.day)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@Serializable
|
||||
data class ALFuzzyDate(
|
||||
@@ -11,10 +12,9 @@ data class ALFuzzyDate(
|
||||
val day: Int?,
|
||||
) {
|
||||
fun toEpochMilli(): Long = try {
|
||||
LocalDate.of(year!!, month!!, day!!)
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
LocalDate(year!!, month!!, day!!)
|
||||
.atStartOfDayIn(TimeZone.currentSystemDefault())
|
||||
.toEpochMilliseconds()
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ data class ALUserViewer(
|
||||
@Serializable
|
||||
data class ALUserViewerData(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val mediaListOptions: ALUserListOptions,
|
||||
)
|
||||
|
||||
|
||||
@@ -106,8 +106,9 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
// Users can set a 'username' (not nickname) once which effectively
|
||||
// replaces the stringified ID in certain queries.
|
||||
// If no username is set, the API returns the user ID as a strings
|
||||
val username = api.getUsername()
|
||||
saveCredentials(username, oauth.accessToken)
|
||||
val currentUser = api.getCurrentUser()
|
||||
saveDisplayUsername(currentUser.nickname?.takeIf { it.isNotBlank() } ?: currentUser.username)
|
||||
saveCredentials(currentUser.username, oauth.accessToken)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
}
|
||||
|
||||
@@ -153,13 +153,12 @@ class BangumiApi(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUsername(): String {
|
||||
suspend fun getCurrentUser(): BGMUser {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$API_URL/v0/me"))
|
||||
.awaitSuccess()
|
||||
.parseAs<BGMUser>()
|
||||
.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
|
||||
// Incomplete DTO with only our needed attributes
|
||||
data class BGMUser(
|
||||
val username: String,
|
||||
val nickname: String?,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package eu.kanade.tachiyomi.data.track.hikka
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.BaseTracker
|
||||
import eu.kanade.tachiyomi.data.track.DeletableTracker
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
|
||||
|
||||
companion object {
|
||||
const val READING = 0L
|
||||
const val COMPLETED = 1L
|
||||
const val ON_HOLD = 2L
|
||||
const val DROPPED = 3L
|
||||
const val PLAN_TO_READ = 4L
|
||||
const val REREADING = 5L
|
||||
|
||||
private val SCORE_LIST = IntRange(0, 10)
|
||||
.map(Int::toString)
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { HikkaInterceptor(this) }
|
||||
|
||||
private val api by lazy { HikkaApi(id, client, interceptor) }
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override fun getLogo(): Int = R.drawable.brand_hikka
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return listOf(
|
||||
READING,
|
||||
COMPLETED,
|
||||
ON_HOLD,
|
||||
DROPPED,
|
||||
PLAN_TO_READ,
|
||||
REREADING,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getStatus(status: Long): StringResource? = when (status) {
|
||||
READING -> MR.strings.reading
|
||||
PLAN_TO_READ -> MR.strings.plan_to_read
|
||||
COMPLETED -> MR.strings.completed
|
||||
ON_HOLD -> MR.strings.on_hold
|
||||
DROPPED -> MR.strings.dropped
|
||||
REREADING -> MR.strings.repeating
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun getReadingStatus(): Long = READING
|
||||
|
||||
override fun getRereadingStatus(): Long = REREADING
|
||||
|
||||
override fun getCompletionStatus(): Long = COMPLETED
|
||||
|
||||
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
|
||||
|
||||
override fun displayScore(track: DomainTrack): String {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
track: Track,
|
||||
didReadChapter: Boolean,
|
||||
): Track {
|
||||
if (track.status != COMPLETED) {
|
||||
if (didReadChapter) {
|
||||
if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) {
|
||||
track.status = COMPLETED
|
||||
track.finished_reading_date = System.currentTimeMillis()
|
||||
} else if (track.status != REREADING) {
|
||||
track.status = READING
|
||||
if (track.last_chapter_read == 1.0) {
|
||||
track.started_reading_date = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.updateUserManga(track)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
val readContent = api.getRead(track)
|
||||
val remoteTrack = api.getManga(track)
|
||||
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.remote_id = remoteTrack.remote_id
|
||||
track.library_id = remoteTrack.library_id
|
||||
|
||||
if (track.status != COMPLETED) {
|
||||
val isRereading = track.status == REREADING
|
||||
track.status = if (!isRereading && hasReadChapters) READING else track.status
|
||||
}
|
||||
|
||||
return if (readContent != null) {
|
||||
track.score = readContent.score.toDouble()
|
||||
track.last_chapter_read = readContent.chapters.toDouble()
|
||||
track.started_reading_date = (readContent.startDate ?: 0L) * 1000
|
||||
track.finished_reading_date = (readContent.endDate ?: 0L) * 1000
|
||||
update(track)
|
||||
} else {
|
||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||
track.score = 0.0
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> = api.searchManga(query)
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getManga(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
|
||||
val readContent = api.getRead(track) ?: throw Exception("Could not find manga")
|
||||
|
||||
track.score = readContent.score.toDouble()
|
||||
track.last_chapter_read = readContent.chapters.toDouble()
|
||||
track.status = toTrackStatus(readContent.status)
|
||||
track.started_reading_date = (readContent.startDate ?: 0L) * 1000
|
||||
track.finished_reading_date = (readContent.endDate ?: 0L) * 1000
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
suspend fun login(reference: String) {
|
||||
try {
|
||||
val oauth = api.accessToken(reference)
|
||||
interceptor.setAuth(oauth)
|
||||
val user = api.getCurrentUser()
|
||||
saveDisplayUsername(user.username)
|
||||
saveCredentials(user.reference, oauth.accessToken)
|
||||
} catch (_: Throwable) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(track: DomainTrack) = api.deleteUserManga(track)
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
trackPreferences.trackToken(this).delete()
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
fun saveOAuth(oAuth: HKOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
|
||||
}
|
||||
|
||||
fun loadOAuth(): HKOAuth? {
|
||||
return try {
|
||||
json.decodeFromString<HKOAuth>(trackPreferences.trackToken(this).get())
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package eu.kanade.tachiyomi.data.track.hikka
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKManga
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKRead
|
||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKUser
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
class HikkaApi(
|
||||
private val trackId: Long,
|
||||
private val client: OkHttpClient,
|
||||
interceptor: HikkaInterceptor,
|
||||
) {
|
||||
suspend fun getCurrentUser(): HKUser {
|
||||
return withIOContext {
|
||||
val request = Request.Builder()
|
||||
.url("${BASE_API_URL}/user/me")
|
||||
.get()
|
||||
.build()
|
||||
with(json) {
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<HKUser>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(reference: String): HKOAuth {
|
||||
return withIOContext {
|
||||
with(json) {
|
||||
client.newCall(authTokenCreate(reference))
|
||||
.awaitSuccess()
|
||||
.parseAs<HKOAuth>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchManga(query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$BASE_API_URL/manga".toUri().buildUpon()
|
||||
.appendQueryParameter("page", "1")
|
||||
.appendQueryParameter("size", "50")
|
||||
.build()
|
||||
|
||||
val payload = buildJsonObject {
|
||||
put("media_type", buildJsonArray { })
|
||||
put("status", buildJsonArray { })
|
||||
put("only_translated", false)
|
||||
put("magazines", buildJsonArray { })
|
||||
put("genres", buildJsonArray { })
|
||||
put(
|
||||
"score",
|
||||
buildJsonArray {
|
||||
add(0)
|
||||
add(10)
|
||||
},
|
||||
)
|
||||
put("query", query)
|
||||
put(
|
||||
"sort",
|
||||
buildJsonArray {
|
||||
add("score:desc")
|
||||
add("scored_by:desc")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
with(json) {
|
||||
authClient.newCall(POST(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<HKMangaPagination>()
|
||||
.list
|
||||
.map { it.toTrack(trackId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRead(track: Track): HKRead? {
|
||||
return withIOContext {
|
||||
val slug = track.tracking_url.split("/")[4]
|
||||
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon().build()
|
||||
with(json) {
|
||||
try {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<HKRead>()
|
||||
} catch (e: HttpException) {
|
||||
if (e.code == 404) {
|
||||
null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getManga(track: Track): TrackSearch {
|
||||
return withIOContext {
|
||||
val slug = track.tracking_url.split("/")[4]
|
||||
val url = "$BASE_API_URL/manga/$slug".toUri().buildUpon()
|
||||
.build()
|
||||
|
||||
with(json) {
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<HKManga>()
|
||||
.toTrack(trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteUserManga(track: DomainTrack) {
|
||||
return withIOContext {
|
||||
val slug = track.remoteUrl.split("/")[4]
|
||||
|
||||
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon()
|
||||
.build()
|
||||
|
||||
authClient.newCall(DELETE(url.toString()))
|
||||
.awaitSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addUserManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val slug = track.tracking_url.split("/")[4]
|
||||
|
||||
val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon()
|
||||
.build()
|
||||
|
||||
var rereads = getRead(track)?.rereads ?: 0
|
||||
if (track.status == Hikka.REREADING && rereads == 0) {
|
||||
rereads = 1
|
||||
}
|
||||
|
||||
val payload = buildJsonObject {
|
||||
put("note", "")
|
||||
put("chapters", track.last_chapter_read.toInt())
|
||||
put("volumes", 0)
|
||||
put("rereads", rereads)
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toApiStatus())
|
||||
put("start_date", if (track.started_reading_date > 0L) track.started_reading_date / 1000 else null)
|
||||
put("end_date", if (track.finished_reading_date > 0L) track.finished_reading_date / 1000 else null)
|
||||
}
|
||||
|
||||
with(json) {
|
||||
authClient.newCall(PUT(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
.parseAs<HKRead>()
|
||||
.toTrack(trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserManga(track: Track): Track = addUserManga(track)
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
companion object {
|
||||
const val BASE_API_URL = "https://api.hikka.io"
|
||||
const val BASE_URL = "https://hikka.io"
|
||||
private const val SCOPE = "readlist,read:user-details"
|
||||
private const val CLIENT_REFERENCE = "598ef1f5-b9d2-4e66-8b65-06949d5e14fc"
|
||||
private const val CLIENT_SECRET = "OKwzrNOZxq40psFgfcCUYddnvaeZWDnd34rt7fdcB5GmHoBBQuNTWX" +
|
||||
"61sZs8KECEWVXtMUDtq8QC4t9WX4DwWWYLXEVlgnlUXGT1fWCb-18c" +
|
||||
"Zd2m8Co-8HN6JQcjoP-B"
|
||||
|
||||
fun authUrl(): Uri = "$BASE_URL/oauth".toUri().buildUpon()
|
||||
.appendQueryParameter("reference", CLIENT_REFERENCE)
|
||||
.appendQueryParameter("scope", SCOPE)
|
||||
.build()
|
||||
|
||||
fun refreshTokenRequest(accessToken: String): Request {
|
||||
val headers = Headers.Builder()
|
||||
.add("auth", accessToken)
|
||||
.build()
|
||||
|
||||
return GET("$BASE_API_URL/user/me", headers = headers) // Any request with auth
|
||||
}
|
||||
|
||||
fun authTokenCreate(reference: String): Request {
|
||||
val payload = buildJsonObject {
|
||||
put("request_reference", reference)
|
||||
put("client_secret", CLIENT_SECRET)
|
||||
}
|
||||
return POST("$BASE_API_URL/auth/token", body = payload.toString().toRequestBody(jsonMime))
|
||||
}
|
||||
|
||||
fun authTokenInfo(accessToken: String): Request {
|
||||
val headers = Headers.Builder()
|
||||
.add("auth", accessToken)
|
||||
.build()
|
||||
|
||||
return GET("$BASE_API_URL/auth/token/info", headers = headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user