Compare commits

...
56 Commits
Author SHA1 Message Date
AntsyLich 81e0754c0d Release v0.20.3 2026-08-04 16:58:34 +06:00
MajorTanyaandAntsyLich e287f08789 Fix MAL error when list entries have partial dates (#3573)
This has the side effect of forcing the date to something concrete.

For Year-only: 1st of January that year
Ex: 2025 -> 2025-01-01

For Year+Month: 1st of said month
Ex: 2024-11 -> 2024-11-01

MAL allows setting Day+Month and Day+Year, but both of those are sent
as `null` in the API, which means Mihon will not show a date at all.
This has the side effect of Mihon sending up the date as `null` the
next time info is sent to MAL, which will clear the date fields
completely.

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-08-04 16:48:08 +06:00
MajorTanya a73271aefa Add category filters to Upcoming calendar (#3607) 2026-08-04 16:30:23 +06:00
AntsyLich f7a1ecd251 Fix reader loading indefinitely in some scenarios (#3686)
Init the reader from the view model instead of the activity so it isn't
cancelled by a configuration change mid init, which left the retained
view model half initialized and needsInit() returning false.

Assisted-by: Claude Code:claude-opus-5
2026-08-03 23:51:17 +06:00
Mend Renovate f80c91081f Update gradle/actions action to v6.3.0 (#3681) 2026-08-03 16:06:15 +06:00
AntsyLich 55be95dd5d Improve WebView spoofing (#3678)
- Spoof `Sec-CH-UA` client hints to match the user agent
- Fix `X-Requested-With` spoofing leaking to unrelated callers
- Update default user agent to Chrome 149
2026-08-02 20:05:21 +06:00
NGB-Was-Taken d7f3ceef5c Ensure R8 keeps methods called externally by Shizuku (#3676)
* Ensure R8 keeps methods called externally by Shizuku

* Changelog
2026-08-02 11:53:49 +00:00
MajorTanya 1d8a2b05dd Add category filters for Updates tab (#3589)
* Add category filters for Updates tab

Works just like Library Updates.

Provides a tab in the Updates tab filter dialog to allow for including
& excluding of updates belonging to a title from a given category.

Excludes overpower Includes, as with Library Updates.

A little helpful text informs users about this.

* Changelog

* [skip ci] Remove unused string

Leftover from before I realized the Default category will always be
available for in-/exclusion.

* Make :includeEmpty/:excludeEmpty true Boolean params

* "include" -> "included"

Consistency with #3607.

* Remove DISTINCT from getRecentUpdatesWithFilters

Apparently not needed anymore (though it was required at some point).

* [skip ci] Fix Changelog
2026-08-02 16:59:58 +06:00
MajorTanya 6d69903a56 Replace java.time APIs with kotlin(x).(date)time (#3001)
* Replace java.time APIs with kotlin(x).(date)time

kotlinx-datetime explicitly says in its README that it does not cover
i18n, so we have to stick with using their toJavaX converters wherever
we localise dates and times.

Yes, some of these replacements are... questionable. But I wanted to
try and maximise the replacement for now.

* Bump kotlinx-datetime to 0.8.0

* More kotlin.time/kotlinx-datetime replacements

* Remove redundant init block & make attributes val

* Replace new use of java.time.Instant
2026-08-02 16:58:25 +06:00
AntsyLich 0648e2eaaf Release v0.20.2 2026-08-01 21:32:00 +06:00
Mend Renovate 7d4e1476ba Update dependency com.google.firebase:firebase-bom to v34.17.0 (#3670) 2026-08-01 21:28:01 +06:00
+9 46a2471abb Translations update from Hosted Weblate (#3563)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/he/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Acelith <joel.jon@moix.me>
Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Cuong Nguyen Viet <vietcuong0501@gmail.com>
Co-authored-by: CyberMageIL <CyberMageIL@Gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Doministo <doministo@seznam.cz>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Hualiang <642615676@qq.com>
Co-authored-by: Infy Milliaria <ced.paltep10@gmail.com>
Co-authored-by: Lucas Correia <anicetoclucas@gmail.com>
Co-authored-by: Luiz Paulo Dias <luizpauloandrade@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: karigane <169052233+karigane-cha@users.noreply.github.com>
Co-authored-by: on9686 <on9686@gmail.com>
Co-authored-by: on9686 <on9686@users.noreply.hosted.weblate.org>
2026-08-01 21:27:45 +06:00
Mend Renovate f63dc799f0 Update actions/setup-java action to v5.7.0 (#3673) 2026-08-01 21:26:51 +06:00
Naji AstierandNaji Astier c114028a33 Fix backup restore dropping library entries (#3667)
* Fix backup restore dropping library entries

History restore resolved chapters by URL alone, so a backup holding the
same entry twice under one source matched multiple chapter rows and threw
`ResultSet returned more than 1 row`. Scope the lookups to the entry.

Entries are restored in chunks of 100 sharing one transaction. SQLDelight
fails the enclosing transaction when a nested one fails, so catching the
per-entry exception did not contain it and the whole chunk rolled back,
losing up to 100 entries per bad one. Retry entry by entry on failure.

Refs #647

* Update changelog

---------

Co-authored-by: Naji Astier <na-ji@users.noreply.github.com>
2026-08-01 21:26:24 +06:00
AntsyLich 4296d056b5 Regenerate baseline profiles 2026-07-30 20:47:17 +06:00
AntsyLich 7aaa891aef Improve app update prompt flow experience (#3669) 2026-07-30 20:36:19 +06:00
Mend Renovate 2ccb0ca163 Update dependency org.jsoup:jsoup to v1.23.1 (#3664) 2026-07-30 18:22:31 +06:00
Mend Renovate 44cd9a55d1 Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 (#3662) 2026-07-29 20:49:55 +00:00
Mend Renovate 2a92f0b46a Update dependency androidx.compose:compose-bom-alpha to v2026.07.01 (#3660) 2026-07-30 02:39:52 +06:00
Mend Renovate f37e81991e Update dependency androidx.baselineprofile to v1.5.0-beta01 (#3659) 2026-07-30 02:38:45 +06:00
AntsyLich d15a05602a Check for app and extension update on every cold start (#3658) 2026-07-30 01:17:10 +06:00
Mend Renovate fbf5ae82f7 Update dependency com.squareup.okio:okio to v3.18.1 (#3654) 2026-07-29 09:48:36 +06:00
Mend Renovate fdb223d657 Update spotless to v8.9.0 (#3652) 2026-07-28 13:39:33 +00:00
NGB-Was-Taken 54ae69eaa2 Remove workarounds for patched compose bugs (#3651) 2026-07-28 19:27:26 +06:00
Mohannad 45b1e781eb Add Tokyo Night theme (#3502) 2026-07-25 20:43:04 +06:00
AntsyLich 069e5c91ac Collect data on device preferred abi
Mainly to analyze if it's worth making binaries for x86 and 32Bit devices
2026-07-25 20:32:23 +06:00
NGB-Was-Taken 4ac9f3474b Fix extension installation with shizuku (#3630)
* Fix extension installation with shizuku

* Changelog

* Move changelog to end
2026-07-25 18:51:11 +06:00
AntsyLich 9c3cc1ca5f Convert :source-api and :source-local to android library module (#3636)
- There's really no plan to support other platforms on this code base
- The "KMP" modules only had android target which is rather pointless
- `i18n` involves more work but given it has no actual code keeping it as is
2026-07-23 16:47:24 +00:00
NGB-Was-Taken 47d7c0f2ea Optimize imports across all modules (#3635)
* Run optimize imports across all modules since spotless doesn't catch unused imports

* Remove import by adding qualifier in KDoc
2026-07-23 22:46:58 +06:00
Mend Renovate 0b5e9bb815 Update android.gradle to v9.3.1 (#3634) 2026-07-23 22:42:29 +06:00
Mend Renovate d72ae365cb Update dependency com.squareup.okio:okio to v3.18.0 (#3628) 2026-07-22 13:28:10 +00:00
NGB-Was-Taken e7ec672c8b Rework library search to use an AST-based approach (#3554)
* Rework library search to use an AST-based approach

* Spotless apply

* Move the filtering logic to the database

* Spotless apply

* Cleanup and separation of concerns

* Add `notes` to field and general search, and chapter read, total chapters to comparison search

* Handle searching for empty fields

* Add `language` as a field-only query option

* Clean up SQL clause generations

* Spotless apply

* Do not parse `-` as a negation when followed by a separator

* Revert moving filtering logic to database

* Changelog

* Use contains for genre instead of equals

* Add special handling for source name

* Add field-only query for source id

* Apply suggestions
2026-07-22 19:20:27 +06:00
Mend Renovate 628cb58862 Update dependency io.kotest:kotest-assertions-core to v6.2.3 (#3622) 2026-07-21 12:09:56 +00:00
MajorTanyaandAntsyLich e454cf8e9d Always use Private installer to update Private extensions (#3614)
* Use Private installer to update Private extensions

This forces the Private installer for any extension which was
installed privately, bypassing whichever setting the user selected in
Settings > Advanced > Installer.

* [skip ci] Changelog entry

* [skip ci] Update CHANGELOG.md

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-07-21 12:02:33 +00:00
Mend Renovate baa0fb7ecb Update dependency androidx.compose:compose-bom-alpha to v2026.07.00 (#3597) 2026-07-21 18:01:02 +06:00
Mend Renovate 2d2cf0879f Update actions/checkout action to v7.0.1 (#3620) 2026-07-21 17:56:46 +06:00
Mohannad 6981185f4b Fix incorrect progress when resuming resumable image downloads (#3616) 2026-07-21 17:46:57 +06:00
MajorTanya 47aa9190d4 Add MangaBaka to README list of trackers (#3615) [skip ci] 2026-07-21 17:44:17 +06:00
AntsyLich 98705910e7 Fix crash caused by inaccessible view model classes during creation (#3609) 2026-07-19 14:06:01 +00:00
Mend Renovate 5ce7d00eb8 Update actions/setup-java action to v5.6.0 (#3602) 2026-07-19 10:45:16 +06:00
Mend Renovate 03229a3807 Update aboutLibraries to v15.0.4 (#3604) 2026-07-18 03:04:07 +06:00
Small_KuandAntsyLich 493140c7ff Detect Shizuku with permission check (#3565)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-07-16 19:52:09 +06:00
AntsyLich 98bb731b4d Remove non cancellable call when trying to init reader 2026-07-16 18:23:43 +06:00
AntsyLich c3b99aea07 Migrate to AndroidX ViewModel from Voyager ScreenModel (#3594) 2026-07-15 21:19:11 +06:00
Mend RenovateandAntsyLich ef1c529672 Update android.gradle to v9.3.0 (#3593)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-07-14 18:32:47 +00:00
Mend Renovate 633eea4ced Update kotlin monorepo to v2.4.10 (#3592) 2026-07-14 18:03:24 +00:00
Mend Renovate 31a4d6ba39 Update softprops/action-gh-release action to v3.0.2 (#3588) 2026-07-14 23:51:37 +06:00
Mend Renovate e9b7c760a1 Update dependency org.conscrypt:conscrypt-android to v2.6.1 (#3587) 2026-07-14 23:51:18 +06:00
Mend Renovate ef1e7ba907 Update dependency org.junit.jupiter:junit-jupiter to v6.1.2 (#3582) 2026-07-12 20:27:17 +06:00
MajorTanya 526d79a975 Fix MangaBaka User-Agent using raw StringResource (#3578)
* Fix MangaBaka User-Agent using raw StringResource

Using the hardcoded app name for the time being until a better
solution can be found.

* Changelog
2026-07-12 06:46:10 +06:00
Mend Renovate b9ac9a0422 Update dependency com.materialkolor:material-kolor to v5.0.0 (#3580) 2026-07-12 06:45:37 +06:00
Draken d171768cff Add Vietnamese translation for fastlane metadata (#3576) 2026-07-11 22:12:39 +06:00
AntsyLich cb2617dee0 Add fastlane metadata (#3564) 2026-07-11 20:01:33 +06:00
AntsyLich b4635c41a8 Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider (#3567) 2026-07-10 13:35:16 +00:00
Mend Renovate 217b20123c Update dependency com.google.firebase:firebase-bom to v34.16.0 (#3562) 2026-07-10 00:09:34 +06:00
AntsyLich b2ca6bc5f3 Revert "Disable resumable image downloads"
This reverts commit 3d3bd49d1c.
2026-07-10 00:08:26 +06:00
330 changed files with 6304 additions and 5480 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.20.1](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.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
+2 -2
View File
@@ -52,7 +52,7 @@ body:
label: Mihon version
description: You can find your Mihon version in **More → About**.
placeholder: |
Example: "0.20.1"
Example: "0.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.1](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 -3
View File
@@ -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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.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
+7 -7
View File
@@ -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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.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 }}
+42 -1
View File
@@ -11,9 +11,48 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [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
@@ -579,7 +618,9 @@ Same as v0.19.6
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.1...main
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.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
+1 -1
View File
@@ -29,7 +29,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
* Local reading of content.
* A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/) and [Hikka](https://hikka.io/) support.
* Tracker support: [MangaBaka](https://mangabaka.org), [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/), and [Hikka](https://hikka.io/) support.
* Categories to organize your library.
* Light and dark themes.
* Schedule updating your library for new chapters.
+5 -2
View File
@@ -32,8 +32,8 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 26
versionName = "0.20.1"
versionCode = 28
versionName = "0.20.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
@@ -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)
+9
View File
@@ -86,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();
}
@@ -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 }
@@ -145,6 +149,7 @@ class SyncChaptersWithSource(
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
timeZone,
now,
fetchWindow,
)
@@ -217,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,
@@ -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(
@@ -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 = {},
)
}
}
@@ -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) {
@@ -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
}
}
@@ -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 ->
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -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,
@@ -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()
}
@@ -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()
}
}
@@ -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 {
@@ -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())
@@ -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(),
@@ -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()
},
@@ -65,6 +65,7 @@ fun ReaderAppBars(
currentPage: Int,
totalPages: Int,
onPageIndexChange: (Int) -> Unit,
onPageIndexChangeFinished: () -> Unit,
readingMode: ReadingMode,
onClickReadingMode: () -> Unit,
@@ -128,6 +129,7 @@ fun ReaderAppBars(
currentPage = currentPage,
totalPages = totalPages,
onPageIndexChange = onPageIndexChange,
onPageIndexChangeFinished = onPageIndexChangeFinished,
)
}
}
@@ -155,6 +157,7 @@ fun ReaderAppBars(
currentPage = currentPage,
totalPages = totalPages,
onPageIndexChange = onPageIndexChange,
onPageIndexChangeFinished = onPageIndexChangeFinished,
)
}
ReaderBottomBar(
@@ -70,6 +70,7 @@ fun ChapterNavigator(
currentPage: Int,
totalPages: Int,
onPageIndexChange: (Int) -> Unit,
onPageIndexChangeFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
val haptic = LocalHapticFeedback.current
@@ -83,6 +84,7 @@ fun ChapterNavigator(
}
state.value = currentPage.toFloat()
state.onValueChange = { onPageIndexChange(it.roundToInt() - 1) }
state.onValueChangeFinished = onPageIndexChangeFinished
val interactionSource = remember { MutableInteractionSource() }
val sliderDragged by interactionSource.collectIsDraggedAsState()
@@ -310,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,7 @@ 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
@@ -33,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)) },
)
}
@@ -59,10 +59,10 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
CheckboxItem(
label = stringResource(MR.strings.pref_show_page_number),
pref = screenModel.preferences.showPageNumber,
pref = viewModel.preferences.showPageNumber,
)
val verticalNavigatorModes by screenModel.preferences.verticalNavigator.collectAsState()
val verticalNavigatorModes by viewModel.preferences.verticalNavigator.collectAsState()
SettingsChipRow(MR.strings.pref_vertical_navigator) {
ReadingMode.entries.filter { it != ReadingMode.DEFAULT }.forEach { mode ->
@@ -74,7 +74,7 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
} else {
verticalNavigatorModes + mode
}
screenModel.preferences.verticalNavigator.set(newModes)
viewModel.preferences.verticalNavigator.set(newModes)
},
label = { Text(stringResource(mode.stringRes)) },
)
@@ -82,12 +82,12 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
}
if (verticalNavigatorModes.isNotEmpty()) {
val verticalNavigatorHeightPref = screenModel.preferences.verticalNavigatorHeight
val verticalNavigatorHeightPref = viewModel.preferences.verticalNavigatorHeight
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
pref = screenModel.preferences.verticalNavigatorOnLeft,
pref = viewModel.preferences.verticalNavigatorOnLeft,
)
SliderItem(
@@ -101,40 +101,40 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
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 }
+1 -2
View File
@@ -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
@@ -148,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}")
}
@@ -162,6 +179,7 @@ class BackupRestorer(
restoreProgress.incrementAndFetch()
}
}
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
}
}
@@ -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
@@ -483,7 +483,7 @@ class Downloader(
?: tmpDir.createFile("$filename.tmp")!!
try {
source.getImage(page).use {
source.getImage(page, file.length()).use {
it.body.source().saveTo(
// If the server supports partial downloads (HTTP 206),
// append to the existing file.
@@ -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) {
@@ -15,6 +15,9 @@ 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
@@ -25,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) {
@@ -326,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
}
@@ -30,7 +30,6 @@ import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.math.RoundingMode
import java.security.SecureRandom
@@ -52,7 +51,7 @@ class MangaBakaApi(
.header(
"User-Agent",
buildString {
append("${MR.strings.app_name}/v${BuildConfig.VERSION_NAME} ")
append("Mihon/v${BuildConfig.VERSION_NAME} ")
append("(${BuildConfig.APPLICATION_ID} ${BuildConfig.COMMIT_SHA}) ")
append("(Android) (https://github.com/mihonapp/mihon)")
},
@@ -245,7 +245,13 @@ class MyAnimeListApi(
}
private fun parseDate(isoDate: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
val pattern = when (isoDate.length) {
10 -> "yyyy-MM-dd"
7 -> "yyyy-MM"
4 -> "yyyy"
else -> throw IllegalArgumentException("Unsupported date format: \"$isoDate\"")
}
return SimpleDateFormat(pattern, Locale.US).parse(isoDate)?.time ?: 0L
}
private fun convertToIsoDate(epochTime: Long): String? {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isFossBuildType
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
@@ -12,7 +11,7 @@ class AppUpdateChecker {
private val getApplicationRelease: GetApplicationRelease by injectLazy()
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
suspend fun checkForUpdate(forceCheck: Boolean = false): GetApplicationRelease.Result {
// Disable app update checks for older Android versions that we're going to drop support for
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// return GetApplicationRelease.Result.OsTooOld
@@ -30,11 +29,6 @@ class AppUpdateChecker {
),
)
when (result) {
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
else -> {}
}
result
}
}
@@ -15,30 +15,26 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import kotlinx.coroutines.CancellationException
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = AppUpdateNotifier(context)
private val network: NetworkHelper by injectLazy()
override suspend fun doWork(): Result {
val url = inputData.getString(EXTRA_DOWNLOAD_URL)
val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.stringResource(MR.strings.app_name)
if (url.isNullOrEmpty()) {
return Result.failure()
@@ -46,17 +42,27 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
setForegroundSafely()
withIOContext {
downloadApk(title, url)
return try {
withIOContext { downloadApk(url) }
Result.success(workDataOf(PROGRESS to 100, EXTRA_DOWNLOAD_URL to url))
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
Result.failure()
}
return Result.success()
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
.setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
.setContentText(context.stringResource(MR.strings.update_check_notification_download_in_progress))
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.build()
return ForegroundInfo(
Notifications.ID_APP_UPDATER,
notifier.onDownloadStarted().build(),
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
@@ -70,10 +76,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
private suspend fun downloadApk(url: String) {
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
@@ -87,45 +90,27 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
notifier.onProgressChange(progress)
setProgressAsync(workDataOf(PROGRESS to progress, EXTRA_DOWNLOAD_URL to url))
}
}
}
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener).awaitSuccess()
// File where the apk will be saved.
val apkFile = File(context.externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
notifier.cancel()
notifier.promptInstall(apkFile.getUriCompat(context))
} catch (e: Exception) {
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
}
}
val apkFile = updateApk(context)
response.body.source().saveTo(apkFile)
}
companion object {
private const val TAG = "AppUpdateDownload"
const val TAG = "AppUpdateDownload"
const val PROGRESS = "progress"
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
fun start(context: Context, url: String, title: String? = null) {
fun updateApk(context: Context): File = File(context.externalCacheDir, "update.apk")
fun start(context: Context, url: String) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
@@ -133,12 +118,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(
workDataOf(
EXTRA_DOWNLOAD_URL to url,
EXTRA_DOWNLOAD_TITLE to title,
),
)
.setInputData(workDataOf(EXTRA_DOWNLOAD_URL to url))
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
@@ -1,168 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.release.model.Release
import tachiyomi.i18n.MR
internal class AppUpdateNotifier(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
/**
* Call to show notification.
*
* @param id id of the notification channel.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_APP_UPDATER) {
context.notify(id, build())
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
@SuppressLint("LaunchActivityFromNotification")
fun promptUpdate(release: Release) {
val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
context,
release.downloadLink,
release.version,
)
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
PendingIntent.getActivity(
context,
release.hashCode(),
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
with(notificationBuilder) {
setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
setContentText(release.version)
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentIntent(updateIntent)
clearActions()
addAction(
android.R.drawable.stat_sys_download_done,
context.stringResource(MR.strings.action_download),
updateIntent,
)
addAction(
R.drawable.ic_info_24dp,
context.stringResource(MR.strings.whats_new),
releaseIntent,
)
}
notificationBuilder.show()
}
/**
* Call when apk download starts.
*
* @param title tile of notification.
*/
fun onDownloadStarted(title: String? = null): NotificationCompat.Builder {
with(notificationBuilder) {
title?.let { setContentTitle(title) }
setContentText(context.stringResource(MR.strings.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
clearActions()
addAction(
R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel),
NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context),
)
}
notificationBuilder.show()
return notificationBuilder
}
/**
* Call when apk download progress changes.
*
* @param progress progress of download (xx%/100).
*/
fun onProgressChange(progress: Int) {
with(notificationBuilder) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notificationBuilder.show()
}
/**
* Call when apk download is finished.
*
* @param uri path location of apk.
*/
fun promptInstall(uri: Uri) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) {
setContentText(context.stringResource(MR.strings.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
setContentIntent(installIntent)
setOngoing(true)
clearActions()
addAction(
R.drawable.ic_system_update_alt_white_24dp,
context.stringResource(MR.strings.action_install),
installIntent,
)
addAction(
R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_PROMPT),
)
}
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
}
/**
* Call when apk download throws a error
*
* @param url web location of apk to download.
*/
fun onDownloadError(url: String) {
with(notificationBuilder) {
setContentText(context.stringResource(MR.strings.update_check_notification_download_error))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
clearActions()
addAction(
R.drawable.ic_refresh_24dp,
context.stringResource(MR.strings.action_retry),
NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url),
)
addAction(
R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
)
}
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
}
}
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.di
import android.app.Application
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import app.cash.sqldelight.db.SqlDriver
@@ -24,9 +25,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.DefaultXmlSerializationPolicy
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlConfig
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
import tachiyomi.data.Chapters
import tachiyomi.data.Database
@@ -55,6 +54,7 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingleton<Context>(app)
addSingletonFactory<SqlDriver> {
synchronized(lock) {
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.domain.upcoming.service.UpcomingPreferences
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
@@ -47,6 +48,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
LibraryPreferences(get())
}
addSingletonFactory {
UpcomingPreferences(get())
}
addSingletonFactory {
UpdatesPreferences(get())
}
@@ -240,7 +240,8 @@ class ExtensionManager(
*/
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
return installExtension(availableExt)
val isUpdateForPrivatelyInstalled = !extension.isShared
return installer.downloadAndInstall(availableExt.apkUrl, availableExt, isUpdateForPrivatelyInstalled)
}
fun cancelInstallUpdateExtension(extension: Extension) {
@@ -1,53 +1,28 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import mihon.domain.extension.interactor.UpdateExtensionStores
import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.time.Instant
import kotlin.time.Duration.Companion.days
internal class ExtensionApi {
private val repository: ExtensionStoreRepository by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val updateExtensionStores: UpdateExtensionStores by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
}
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext { repository.fetchExtensions() }
}
suspend fun checkForUpdates(
context: Context,
fromAvailableExtensionList: Boolean = false,
): List<Extension.Installed>? {
// Limit checks to once a day at most
if (!fromAvailableExtensionList &&
Instant.now().toEpochMilli() < lastExtCheck.get() + 1.days.inWholeMilliseconds
) {
return null
}
suspend fun checkForUpdates(context: Context) {
updateExtensionStores()
val extensions = if (fromAvailableExtensionList) {
extensionManager.availableExtensionsFlow.value
} else {
findExtensions().also { lastExtCheck.set(Instant.now().toEpochMilli()) }
}
val extensions = findExtensions()
val installedExtensions = ExtensionLoader.loadExtensions(context)
.filterIsInstance<LoadResult.Success>()
@@ -68,7 +43,5 @@ internal class ExtensionApi {
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
}
return extensionsWithUpdate
}
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
@@ -39,6 +40,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
.processNameSuffix("shizuku_service")
.debuggable(BuildConfig.DEBUG)
.daemon(false)
.version(2)
}
private val connection = object : ServiceConnection {
@@ -53,6 +55,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
}
}
private val statusIntent = PendingIntent.getBroadcast(
service.applicationContext,
0,
Intent(ACTION_INSTALL_RESULT).setPackage(BuildConfig.APPLICATION_ID),
PendingIntent.FLAG_MUTABLE,
)
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)
@@ -110,7 +119,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
super.processEntry(entry)
try {
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
shellInterface?.install(it)
shellInterface?.install(it, statusIntent.intentSender)
}
service.contentResolver.delete(entry.uri, null, null)
} catch (e: Exception) {
@@ -49,8 +49,13 @@ internal class ExtensionInstaller(
*
* @param url The url of the apk.
* @param extension The extension to install.
* @param isUpdateForPrivatelyInstalled If this is an update for a privately installed extension
*/
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
fun downloadAndInstall(
url: String,
extension: Extension,
isUpdateForPrivatelyInstalled: Boolean = false,
): Flow<InstallStep> {
val downloadId = extension.pkgName.hashCode().toLong()
cancelInstall(extension.pkgName)
@@ -74,7 +79,7 @@ internal class ExtensionInstaller(
}
step.value = InstallStep.Installing
installApk(downloadId, tmpFile)
installApk(downloadId, tmpFile, isUpdateForPrivatelyInstalled)
} catch (e: Exception) {
if (e is InterruptedException) {
// Canceled
@@ -99,8 +104,14 @@ internal class ExtensionInstaller(
* Starts an intent to install the extension at the given uri.
*
* @param tempFile The file of the extension to install. Delete after use.
* @param isUpdateForPrivatelyInstalled If this install is an update for a privately installed extension
*/
private fun installApk(downloadId: Long, tempFile: File) {
private fun installApk(downloadId: Long, tempFile: File, isUpdateForPrivatelyInstalled: Boolean = false) {
if (isUpdateForPrivatelyInstalled) {
installApkPrivately(downloadId, tempFile)
return
}
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, ExtensionInstallActivity::class.java)
@@ -110,20 +121,11 @@ internal class ExtensionInstaller(
context.startActivity(intent)
}
BasePreferences.ExtensionInstaller.PRIVATE -> {
try {
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
updateInstallStep(downloadId, InstallStep.Installed)
} else {
updateInstallStep(downloadId, InstallStep.Error)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
updateInstallStep(downloadId, InstallStep.Error)
}
tempFile.delete()
BasePreferences.ExtensionInstaller.PRIVATE -> {
installApkPrivately(downloadId, tempFile)
}
else -> {
val intent = ExtensionInstallService.getIntent(
context,
@@ -136,6 +138,21 @@ internal class ExtensionInstaller(
}
}
private fun installApkPrivately(downloadId: Long, tempFile: File) {
try {
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
updateInstallStep(downloadId, InstallStep.Installed)
} else {
updateInstallStep(downloadId, InstallStep.Error)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
updateInstallStep(downloadId, InstallStep.Error)
}
tempFile.delete()
}
/**
* Cancels extension install and remove from download manager and installer.
*/
@@ -31,6 +31,7 @@ class ThemingDelegateImpl : ThemingDelegate {
private val themeResources: Map<AppTheme, Int> = mapOf(
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
AppTheme.CATPPUCCIN to R.style.Theme_Tachiyomi_Catppuccin,
AppTheme.TOKYONIGHT to R.style.Theme_Tachiyomi_TokyoNight,
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
@@ -9,14 +9,14 @@ 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.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
import eu.kanade.tachiyomi.ui.browse.extension.extensionsTab
import eu.kanade.tachiyomi.ui.browse.migration.sources.migrateSourceTab
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
@@ -58,12 +58,12 @@ data object BrowseTab : Tab {
val context = LocalContext.current
// Hoisted for extensions tab's search bar
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() }
val extensionsState by extensionsScreenModel.state.collectAsState()
val extensionsViewModel = viewModel<ExtensionsViewModel>()
val extensionsState by extensionsViewModel.state.collectAsState()
val tabs = listOf(
sourcesTab(),
extensionsTab(extensionsScreenModel),
extensionsTab(extensionsViewModel),
migrateSourceTab(),
)
@@ -74,7 +74,7 @@ data object BrowseTab : Tab {
tabs = tabs,
state = state,
searchQuery = extensionsState.searchQuery,
onChangeSearchQuery = extensionsScreenModel::search,
onChangeSearchQuery = extensionsViewModel::search,
)
LaunchedEffect(Unit) {
switchToExtensionTabChannel.receiveAsFlow()
@@ -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.browse.ExtensionFilterScreen
@@ -21,8 +21,8 @@ class ExtensionFilterScreen : Screen() {
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ExtensionFilterScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<ExtensionFilterViewModel>()
val state by viewModel.state.collectAsState()
if (state is ExtensionFilterState.Loading) {
LoadingScreen()
@@ -34,11 +34,11 @@ class ExtensionFilterScreen : Screen() {
ExtensionFilterScreen(
navigateUp = navigator::pop,
state = successState,
onClickToggle = screenModel::toggle,
onClickToggle = viewModel::toggle,
)
LaunchedEffect(Unit) {
screenModel.events.collectLatest {
viewModel.events.collectLatest {
when (it) {
ExtensionFilterEvent.FailedFetchingLanguages -> {
context.stringResource(MR.strings.internal_error)
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.extension
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.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.service.SourcePreferences
@@ -15,21 +14,22 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionFilterScreenModel(
class ExtensionFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateScreenModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
) : StateViewModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
private val _events: Channel<ExtensionFilterEvent> = Channel()
val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
combine(
getExtensionLanguages.subscribe(),
preferences.enabledLanguages.changes(),
@@ -26,12 +26,12 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun extensionsTab(
extensionsScreenModel: ExtensionsScreenModel,
extensionsViewModel: ExtensionsViewModel,
): TabContent {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val state by extensionsScreenModel.state.collectAsState()
val state by extensionsViewModel.state.collectAsState()
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
return TabContent(
@@ -50,7 +50,7 @@ fun extensionsTab(
),
content = { contentPadding, _ ->
BackHandler(enabled = state.searchQuery != null) {
extensionsScreenModel.search(null)
extensionsViewModel.search(null)
}
ExtensionScreen(
@@ -59,18 +59,18 @@ fun extensionsTab(
searchQuery = state.searchQuery,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> extensionsScreenModel.installExtension(extension)
is Extension.Available -> extensionsViewModel.installExtension(extension)
else -> {
if (context.isPackageInstalled(extension.pkgName)) {
extensionsScreenModel.uninstallExtension(extension)
extensionsViewModel.uninstallExtension(extension)
} else {
privateExtensionToUninstall = extension
}
}
}
},
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
onClickItemCancel = extensionsViewModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsViewModel::updateAllExtensions,
onOpenWebView = { extension ->
extension.sources.getOrNull(0)?.let {
navigator.push(
@@ -82,19 +82,19 @@ fun extensionsTab(
)
}
},
onInstallExtension = extensionsScreenModel::installExtension,
onInstallExtension = extensionsViewModel::installExtension,
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
onTrustExtension = { extensionsScreenModel.trustExtension(it) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions,
onTrustExtension = { extensionsViewModel.trustExtension(it) },
onUninstallExtension = { extensionsViewModel.uninstallExtension(it) },
onUpdateExtension = extensionsViewModel::updateExtension,
onRefresh = extensionsViewModel::findAvailableExtensions,
)
privateExtensionToUninstall?.let { extension ->
ExtensionUninstallConfirmation(
extensionName = extension.name,
onClickConfirm = {
extensionsScreenModel.uninstallExtension(extension)
extensionsViewModel.uninstallExtension(extension)
},
onDismissRequest = {
privateExtensionToUninstall = null
@@ -2,8 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import androidx.lifecycle.viewModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.interactor.GetExtensionsByType
@@ -28,18 +27,19 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
class ExtensionsScreenModel(
class ExtensionsViewModel(
preferences: SourcePreferences = Injekt.get(),
basePreferences: BasePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(),
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
) : StateViewModel<ExtensionsViewModel.State>(State()) {
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
@@ -51,7 +51,7 @@ class ExtensionsScreenModel(
}
}
screenModelScope.launchIO {
viewModelScope.launchIO {
combine(
state.map { it.searchQuery }
.distinctUntilChanged()
@@ -95,15 +95,15 @@ class ExtensionsScreenModel(
}
}
screenModelScope.launchIO { findAvailableExtensions() }
viewModelScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount.changes()
.onEach { mutableState.update { state -> state.copy(updates = it) } }
.launchIn(screenModelScope)
.launchIn(viewModelScope)
basePreferences.extensionInstaller.changes()
.onEach { mutableState.update { state -> state.copy(installer = it) } }
.launchIn(screenModelScope)
.launchIn(viewModelScope)
}
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
@@ -143,7 +143,7 @@ class ExtensionsScreenModel(
}
fun updateAllExtensions() {
screenModelScope.launchIO {
viewModelScope.launchIO {
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<Extension.Installed>()
@@ -153,13 +153,13 @@ class ExtensionsScreenModel(
}
fun installExtension(extension: Extension.Available) {
screenModelScope.launchIO {
viewModelScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
}
}
fun updateExtension(extension: Extension.Installed) {
screenModelScope.launchIO {
viewModelScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
}
@@ -189,7 +189,7 @@ class ExtensionsScreenModel(
}
fun findAvailableExtensions() {
screenModelScope.launchIO {
viewModelScope.launchIO {
mutableState.update { it.copy(isRefreshing = true) }
extensionManager.findAvailableExtensions()
@@ -202,7 +202,7 @@ class ExtensionsScreenModel(
}
fun trustExtension(extension: Extension.Untrusted) {
screenModelScope.launch {
viewModelScope.launch {
extensionManager.trust(extension)
}
}
@@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable
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.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionDetailsScreen
@@ -19,9 +19,13 @@ data class ExtensionDetailsScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val screenModel = rememberScreenModel { ExtensionDetailsScreenModel(pkgName = pkgName, context = context) }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<ExtensionDetailsViewModel>(
factory = ExtensionDetailsViewModel.Factory,
extras = CreationExtras {
set(ExtensionDetailsViewModel.PKG_NAME_KEY, pkgName)
},
)
val state by viewModel.state.collectAsState()
if (state.isLoading) {
LoadingScreen()
@@ -34,16 +38,16 @@ data class ExtensionDetailsScreen(
navigateUp = navigator::pop,
state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickEnableAll = { screenModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies,
onClickUninstall = screenModel::uninstallExtension,
onClickSource = screenModel::toggleSource,
onClickIncognito = screenModel::toggleIncognito,
onClickEnableAll = { viewModel.toggleSources(true) },
onClickDisableAll = { viewModel.toggleSources(false) },
onClickClearCookies = viewModel::clearCookies,
onClickUninstall = viewModel::uninstallExtension,
onClickSource = viewModel::toggleSource,
onClickIncognito = viewModel::toggleIncognito,
)
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
viewModel.events.collectLatest { event ->
if (event is ExtensionDetailsEvent.Uninstalled) {
navigator.pop()
}
@@ -1,9 +1,12 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleIncognito
@@ -24,12 +27,13 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import okhttp3.HttpUrl.Companion.toHttpUrl
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionDetailsScreenModel(
class ExtensionDetailsViewModel(
pkgName: String,
context: Context,
private val network: NetworkHelper = Injekt.get(),
@@ -38,13 +42,26 @@ class ExtensionDetailsScreenModel(
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<ExtensionDetailsScreenModel.State>(State()) {
) : StateViewModel<ExtensionDetailsViewModel.State>(State()) {
companion object {
val PKG_NAME_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
ExtensionDetailsViewModel(
pkgName = get(PKG_NAME_KEY)!!,
context = Injekt.get<Application>(),
)
}
}
}
private val _events: Channel<ExtensionDetailsEvent> = Channel()
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
launch {
extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
@@ -19,7 +19,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
@@ -47,9 +48,14 @@ data class MigrateMangaScreen(
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateMangaScreenModel(sourceId) }
val viewModel = viewModel<MigrateMangaViewModel>(
factory = MigrateMangaViewModel.Factory,
extras = CreationExtras {
set(MigrateMangaViewModel.SOURCE_ID_KEY, sourceId)
},
)
val state by screenModel.state.collectAsState()
val state by viewModel.state.collectAsState()
if (state.isLoading) {
LoadingScreen()
@@ -57,7 +63,7 @@ data class MigrateMangaScreen(
}
BackHandler(enabled = state.selectionMode) {
screenModel.clearSelection()
viewModel.clearSelection()
}
val lazyListState = rememberLazyListState()
@@ -68,7 +74,7 @@ data class MigrateMangaScreen(
title = state.source!!.name,
navigateUp = {
if (state.selectionMode) {
screenModel.clearSelection()
viewModel.clearSelection()
} else {
navigator.pop()
}
@@ -84,7 +90,7 @@ data class MigrateMangaScreen(
},
onClick = {
val selection = state.selection
screenModel.clearSelection()
viewModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
expanded = lazyListState.shouldExpandFAB(),
@@ -107,13 +113,13 @@ data class MigrateMangaScreen(
lazyListState = lazyListState,
contentPadding = contentPadding,
state = state,
onClickItem = screenModel::toggleSelection,
onClickItem = viewModel::toggleSelection,
onClickCover = { navigator.push(MangaScreen(it.id)) },
)
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
viewModel.events.collectLatest { event ->
when (event) {
MigrationMangaEvent.FailedFetchingFavorites -> {
context.toast(MR.strings.internal_error)
@@ -127,7 +133,7 @@ data class MigrateMangaScreen(
private fun MigrateMangaContent(
lazyListState: LazyListState,
contentPadding: PaddingValues,
state: MigrateMangaScreenModel.State,
state: MigrateMangaViewModel.State,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
@@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
@@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.common.utils.mutate
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
@@ -21,17 +24,29 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateMangaScreenModel(
class MigrateMangaViewModel(
private val sourceId: Long,
private val sourceManager: SourceManager = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(),
) : StateScreenModel<MigrateMangaScreenModel.State>(State()) {
) : StateViewModel<MigrateMangaViewModel.State>(State()) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateMangaViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
private val _events: Channel<MigrationMangaEvent> = Channel()
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
mutableState.update { state ->
state.copy(source = sourceManager.getOrStub(sourceId))
}
@@ -3,12 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.feature.migration.list.MigrationListScreen
@@ -19,18 +20,23 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<MigrateSearchViewModel>(
factory = MigrateSearchViewModel.Factory,
extras = CreationExtras {
set(MigrateSearchViewModel.MANGA_ID_KEY, mangaId)
},
)
val state by viewModel.state.collectAsState()
MigrateSearchScreen(
state = state,
fromSourceId = state.from?.source,
navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = { screenModel.search() },
getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onChangeSearchQuery = viewModel::updateSearchQuery,
onSearch = { viewModel.search() },
getManga = { viewModel.getManga(it) },
onChangeSearchFilter = viewModel::setSourceFilter,
onToggleResults = viewModel::toggleFilterResults,
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
onClickItem = {
val migrateListScreen = navigator.items
@@ -38,7 +44,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
.lastOrNull()
if (migrateListScreen == null) {
screenModel.setMigrateDialog(mangaId, it)
viewModel.setMigrateDialog(mangaId, it)
} else {
migrateListScreen.addMatchOverride(current = mangaId, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen }
@@ -48,13 +54,13 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
)
when (val dialog = state.dialog) {
is SearchScreenModel.Dialog.Migrate -> {
is SearchViewModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
// Initiated from the context of [dialog.current] so we show [dialog.target].
onClickTitle = { navigator.push(MangaScreen(dialog.target.id, true)) },
onDismissRequest = { screenModel.clearDialog() },
onDismissRequest = { viewModel.clearDialog() },
onComplete = {
if (navigator.lastItem is MangaScreen) {
val lastItem = navigator.lastItem

Some files were not shown because too many files have changed in this diff Show More