Compare commits

...
47 Commits
Author SHA1 Message Date
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
274 changed files with 5278 additions and 5051 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.2](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.2"
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.2](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
+3 -2
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,14 +27,14 @@ 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
+5 -5
View File
@@ -32,10 +32,10 @@ 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
@@ -80,10 +80,10 @@ 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
@@ -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 }}
+24 -1
View File
@@ -11,9 +11,31 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [Unreleased]
## [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 +601,8 @@ 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.2...main
[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.
+3 -2
View File
@@ -32,8 +32,8 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 26
versionName = "0.20.1"
versionCode = 27
versionName = "0.20.2"
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)
@@ -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()) }
@@ -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()
}
}
@@ -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,7 @@ 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 tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@@ -32,13 +31,13 @@ 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,7 +1,7 @@
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 tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.domain.manga.model.MangaCover
import java.time.Instant
@@ -10,9 +10,9 @@ import java.time.temporal.ChronoUnit
import java.util.Date
import kotlin.random.Random
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)
@@ -26,7 +26,7 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
dialog = null,
)
private val shortRecent = HistoryScreenModel.State(
private val shortRecent = HistoryViewModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
@@ -35,7 +35,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 +44,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,
@@ -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
@@ -79,7 +79,7 @@ import java.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(
@@ -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 = {},
)
}
}
@@ -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)
}
@@ -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,17 +19,19 @@ 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
@@ -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()) {
@@ -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),
)
}
@@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsViewModel
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.updates.service.UpdatesPreferences
import tachiyomi.i18n.MR
@@ -32,7 +32,7 @@ import tachiyomi.presentation.core.util.collectAsState
@Composable
fun UpdatesFilterDialog(
onDismissRequest: () -> Unit,
screenModel: UpdatesSettingsScreenModel,
viewModel: UpdatesSettingsViewModel,
) {
TabbedDialog(
onDismissRequest = onDismissRequest,
@@ -45,48 +45,48 @@ fun UpdatesFilterDialog(
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
FilterSheet(screenModel = screenModel)
FilterSheet(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
@@ -29,7 +29,7 @@ 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 tachiyomi.i18n.MR
@@ -45,7 +45,7 @@ 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?
}
@@ -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)
}
}
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
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
@@ -285,7 +284,7 @@ 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)
return manga
@@ -325,16 +324,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.
@@ -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
@@ -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)")
},
@@ -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) {
@@ -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
@@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
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.source.service.SourcePreferences
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 kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
@@ -12,12 +15,24 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateSearchScreenModel(
class MigrateSearchViewModel(
val mangaId: Long,
getManga: GetManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : SearchScreenModel() {
) : SearchViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateSearchViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
}
private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
@@ -29,7 +44,7 @@ class MigrateSearchScreenModel(
}
init {
screenModelScope.launch {
viewModelScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {
it.copy(
@@ -17,7 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel
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.core.util.ifSourcesLoaded
@@ -25,7 +26,7 @@ import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
@@ -59,8 +60,14 @@ data class MigrateSourceSearchScreen(
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, query) }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<BrowseSourceViewModel>(
factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, query)
},
)
val state by viewModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
@@ -68,9 +75,9 @@ data class MigrateSourceSearchScreen(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = state.toolbarQuery ?: "",
onChangeSearchQuery = screenModel::setToolbarQuery,
onChangeSearchQuery = viewModel::setToolbarQuery,
onClickCloseSearch = navigator::pop,
onSearch = screenModel::search,
onSearch = viewModel::search,
scrollBehavior = scrollBehavior,
)
},
@@ -78,7 +85,7 @@ data class MigrateSourceSearchScreen(
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
onClick = screenModel::openFilterSheet,
onClick = viewModel::openFilterSheet,
modifier = Modifier.animateFloatingActionButton(
visible = state.filters.isNotEmpty(),
alignment = Alignment.BottomEnd,
@@ -93,21 +100,21 @@ data class MigrateSourceSearchScreen(
.lastOrNull()
if (migrateListScreen == null) {
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(target = it, current = currentManga))
viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(target = it, current = currentManga))
} else {
migrateListScreen.addMatchOverride(current = currentManga.id, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen }
}
}
BrowseSourceContent(
source = screenModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
source = viewModel.source,
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = viewModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = {
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
val source = viewModel.source as? HttpSource ?: return@BrowseSourceContent
navigator.push(
WebViewScreen(
url = source.getHomeUrl(),
@@ -123,18 +130,18 @@ data class MigrateSourceSearchScreen(
)
}
val onDismissRequest = { screenModel.setDialog(null) }
val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Filter -> {
is BrowseSourceViewModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
onReset = screenModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters,
onReset = viewModel::resetFilters,
onFilter = { viewModel.search(filters = state.filters) },
onUpdate = viewModel::setFilters,
)
}
is BrowseSourceScreenModel.Dialog.Migrate -> {
is BrowseSourceViewModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = currentManga,
target = dialog.target,
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@@ -21,8 +21,8 @@ import tachiyomi.presentation.core.i18n.stringResource
fun Screen.migrateSourceTab(): TabContent {
val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<MigrateSourceViewModel>()
val state by viewModel.state.collectAsState()
return TabContent(
titleRes = MR.strings.label_migration,
@@ -42,8 +42,8 @@ fun Screen.migrateSourceTab(): TabContent {
onClickItem = { source ->
navigator.push(MigrateMangaScreen(source.id))
},
onToggleSortingDirection = screenModel::toggleSortingDirection,
onToggleSortingMode = screenModel::toggleSortingMode,
onToggleSortingDirection = viewModel::toggleSortingDirection,
onToggleSortingMode = viewModel::toggleSortingMode,
)
},
)
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
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.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences
@@ -14,23 +13,24 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateSourceScreenModel(
class MigrateSourceViewModel(
preferences: SourcePreferences = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : StateScreenModel<MigrateSourceScreenModel.State>(State()) {
) : StateViewModel<MigrateSourceViewModel.State>(State()) {
private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow()
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
getSourcesWithFavoriteCount.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
@@ -48,11 +48,11 @@ class MigrateSourceScreenModel(
preferences.migrationSortingDirection.changes()
.onEach { mutableState.update { state -> state.copy(sortingDirection = it) } }
.launchIn(screenModelScope)
.launchIn(viewModelScope)
preferences.migrationSortingMode.changes()
.onEach { mutableState.update { state -> state.copy(sortingMode = it) } }
.launchIn(screenModelScope)
.launchIn(viewModelScope)
}
fun toggleSortingMode() {
@@ -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.SourcesFilterScreen
@@ -19,15 +19,15 @@ class SourcesFilterScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourcesFilterScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<SourcesFilterViewModel>()
val state by viewModel.state.collectAsState()
if (state is SourcesFilterScreenModel.State.Loading) {
if (state is SourcesFilterViewModel.State.Loading) {
LoadingScreen()
return
}
if (state is SourcesFilterScreenModel.State.Error) {
if (state is SourcesFilterViewModel.State.Error) {
val context = LocalContext.current
LaunchedEffect(Unit) {
context.toast(MR.strings.internal_error)
@@ -36,13 +36,13 @@ class SourcesFilterScreen : Screen() {
return
}
val successState = state as SourcesFilterScreenModel.State.Success
val successState = state as SourcesFilterViewModel.State.Success
SourcesFilterScreen(
navigateUp = navigator::pop,
state = successState,
onClickLanguage = screenModel::toggleLanguage,
onClickSource = screenModel::toggleSource,
onClickLanguage = viewModel::toggleLanguage,
onClickSource = viewModel::toggleSource,
)
}
}
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source
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.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
@@ -12,20 +11,21 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.domain.source.model.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.SortedMap
class SourcesFilterScreenModel(
class SourcesFilterViewModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) {
) : StateViewModel<SourcesFilterViewModel.State>(State.Loading) {
init {
screenModelScope.launch {
viewModelScope.launch {
combine(
getLanguagesWithSources.subscribe(),
preferences.enabledLanguages.changes(),
@@ -7,7 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.rememberScreenModel
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@@ -25,8 +25,8 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun Screen.sourcesTab(): TabContent {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { SourcesScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<SourcesViewModel>()
val state by viewModel.state.collectAsState()
return TabContent(
titleRes = MR.strings.label_sources,
@@ -49,8 +49,8 @@ fun Screen.sourcesTab(): TabContent {
onClickItem = { source, listing ->
navigator.push(BrowseSourceScreen(source.id, listing.query))
},
onClickPin = screenModel::togglePin,
onLongClickItem = screenModel::showSourceDialog,
onClickPin = viewModel::togglePin,
onLongClickItem = viewModel::showSourceDialog,
)
state.dialog?.let { dialog ->
@@ -58,22 +58,22 @@ fun Screen.sourcesTab(): TabContent {
SourceOptionsDialog(
source = source,
onClickPin = {
screenModel.togglePin(source)
screenModel.closeDialog()
viewModel.togglePin(source)
viewModel.closeDialog()
},
onClickDisable = {
screenModel.toggleSource(source)
screenModel.closeDialog()
viewModel.toggleSource(source)
viewModel.closeDialog()
},
onDismiss = screenModel::closeDialog,
onDismiss = viewModel::closeDialog,
)
}
val internalErrString = stringResource(MR.strings.internal_error)
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
viewModel.events.collectLatest { event ->
when (event) {
SourcesScreenModel.Event.FailedFetchingSources -> {
SourcesViewModel.Event.FailedFetchingSources -> {
launch { snackbarHostState.showSnackbar(internalErrString) }
}
}
@@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source
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.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.Pin
@@ -21,17 +21,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.TreeMap
class SourcesScreenModel(
class SourcesViewModel(
private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : StateScreenModel<SourcesScreenModel.State>(State()) {
) : StateViewModel<SourcesViewModel.State>(State()) {
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
getEnabledSources.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
@@ -32,7 +32,8 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
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.core.util.ifSourcesLoaded
@@ -46,7 +47,7 @@ import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
@@ -81,20 +82,26 @@ data class BrowseSourceScreen(
return
}
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, listingQuery) }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<BrowseSourceViewModel>(
factory = BrowseSourceViewModel.Factory,
extras = CreationExtras {
set(BrowseSourceViewModel.SOURCE_ID_KEY, sourceId)
set(BrowseSourceViewModel.LISTING_QUERY_KEY, listingQuery)
},
)
val state by viewModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = {
when {
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null)
!state.isUserQuery && state.toolbarQuery != null -> viewModel.setToolbarQuery(null)
else -> navigator.pop()
}
}
if (screenModel.source is StubSource) {
if (viewModel.source is StubSource) {
MissingSourceScreen(
source = screenModel.source,
source = viewModel.source,
navigateUp = navigateUp,
)
return
@@ -107,7 +114,7 @@ data class BrowseSourceScreen(
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f
val source = viewModel.source as? HttpSource ?: return@f
navigator.push(
WebViewScreen(
url = source.getHomeUrl(),
@@ -117,8 +124,8 @@ data class BrowseSourceScreen(
)
}
LaunchedEffect(screenModel.source) {
assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl()
LaunchedEffect(viewModel.source) {
assistUrl = (viewModel.source as? HttpSource)?.getHomeUrl()
}
Scaffold(
@@ -130,15 +137,15 @@ data class BrowseSourceScreen(
) {
BrowseSourceToolbar(
searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery,
source = screenModel.source,
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
onSearchQueryChange = viewModel::setToolbarQuery,
source = viewModel.source,
displayMode = viewModel.displayMode,
onDisplayModeChange = { viewModel.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
onSearch = screenModel::search,
onSearch = viewModel::search,
)
Row(
@@ -150,8 +157,8 @@ data class BrowseSourceScreen(
FilterChip(
selected = state.listing == Listing.Popular,
onClick = {
screenModel.resetFilters()
screenModel.setListing(Listing.Popular)
viewModel.resetFilters()
viewModel.setListing(Listing.Popular)
},
leadingIcon = {
Icon(
@@ -165,12 +172,12 @@ data class BrowseSourceScreen(
Text(text = stringResource(MR.strings.popular))
},
)
if (screenModel.source.supportsLatest) {
if (viewModel.source.supportsLatest) {
FilterChip(
selected = state.listing == Listing.Latest,
onClick = {
screenModel.resetFilters()
screenModel.setListing(Listing.Latest)
viewModel.resetFilters()
viewModel.setListing(Listing.Latest)
},
leadingIcon = {
Icon(
@@ -188,7 +195,7 @@ data class BrowseSourceScreen(
if (state.filters.isNotEmpty()) {
FilterChip(
selected = state.listing is Listing.Search,
onClick = screenModel::openFilterSheet,
onClick = viewModel::openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
@@ -210,10 +217,10 @@ data class BrowseSourceScreen(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
BrowseSourceContent(
source = screenModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
source = viewModel.source,
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = viewModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
@@ -222,13 +229,13 @@ data class BrowseSourceScreen(
onMangaClick = { navigator.push((MangaScreen(it.id, true))) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicates = screenModel.getDuplicateLibraryManga(manga)
val duplicates = viewModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicates.isNotEmpty() -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
manga.favorite -> viewModel.setDialog(BrowseSourceViewModel.Dialog.RemoveManga(manga))
duplicates.isNotEmpty() -> viewModel.setDialog(
BrowseSourceViewModel.Dialog.AddDuplicateManga(manga, duplicates),
)
else -> screenModel.addFavorite(manga)
else -> viewModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
@@ -236,28 +243,28 @@ data class BrowseSourceScreen(
)
}
val onDismissRequest = { screenModel.setDialog(null) }
val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Filter -> {
is BrowseSourceViewModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
onReset = screenModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters,
onReset = viewModel::resetFilters,
onFilter = { viewModel.search(filters = state.filters) },
onUpdate = viewModel::setFilters,
)
}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
is BrowseSourceViewModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onConfirm = { viewModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) },
onMigrate = { viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(dialog.manga, it)) },
)
}
is BrowseSourceScreenModel.Dialog.Migrate -> {
is BrowseSourceViewModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
@@ -266,23 +273,23 @@ data class BrowseSourceScreen(
onDismissRequest = onDismissRequest,
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
is BrowseSourceViewModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
viewModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
is BrowseSourceViewModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
viewModel.changeMangaFavorite(dialog.manga)
viewModel.moveMangaToCategories(dialog.manga, include)
},
)
}
@@ -293,8 +300,8 @@ data class BrowseSourceScreen(
queryEvent.receiveAsFlow()
.collectLatest {
when (it) {
is SearchType.Genre -> screenModel.searchGenre(it.txt)
is SearchType.Text -> screenModel.search(it.txt)
is SearchType.Genre -> viewModel.searchGenre(it.txt)
is SearchType.Text -> viewModel.search(it.txt)
}
}
}
@@ -6,19 +6,20 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers
@@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
@@ -50,7 +52,7 @@ import uy.kohesive.injekt.api.get
import java.time.Instant
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
class BrowseSourceScreenModel(
class BrowseSourceViewModel(
private val sourceId: Long,
listingQuery: String?,
sourceManager: SourceManager = Injekt.get(),
@@ -66,9 +68,23 @@ class BrowseSourceScreenModel(
private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
getIncognitoState: GetIncognitoState = Injekt.get(),
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
) : StateViewModel<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery))) {
var displayMode by sourcePreferences.sourceDisplayMode.asState(screenModelScope)
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val LISTING_QUERY_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
BrowseSourceViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
listingQuery = get(LISTING_QUERY_KEY),
)
}
}
}
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
val source = sourceManager.getOrStub(sourceId)
@@ -107,13 +123,13 @@ class BrowseSourceScreenModel(
pagingData.map { manga ->
getManga.subscribe(manga.url, manga.source)
.map { it ?: manga }
.stateIn(ioCoroutineScope)
.stateIn(viewModelScope)
}
.filter { !hideInLibraryItems || !it.value.favorite }
}
.cachedIn(ioCoroutineScope)
.cachedIn(viewModelScope)
}
.stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow())
.stateIn(viewModelScope, SharingStarted.Lazily, emptyFlow())
fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
@@ -205,7 +221,7 @@ class BrowseSourceScreenModel(
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
screenModelScope.launch {
viewModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
@@ -226,7 +242,7 @@ class BrowseSourceScreenModel(
}
fun addFavorite(manga: Manga) {
screenModelScope.launch {
viewModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory.get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@@ -281,7 +297,7 @@ class BrowseSourceScreenModel(
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO {
viewModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
@@ -7,7 +7,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.core.util.ifSourcesLoaded
@@ -31,13 +32,14 @@ class GlobalSearchScreen(
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
GlobalSearchScreenModel(
initialQuery = searchQuery,
initialExtensionFilter = extensionFilter,
)
}
val state by screenModel.state.collectAsState()
val viewModel = viewModel<GlobalSearchViewModel>(
factory = GlobalSearchViewModel.Factory,
extras = CreationExtras {
set(GlobalSearchViewModel.INITIAL_QUERY_KEY, searchQuery)
set(GlobalSearchViewModel.INITIAL_EXTENSION_FILTER_KEY, extensionFilter)
},
)
val state by viewModel.state.collectAsState()
var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
}
@@ -64,11 +66,11 @@ class GlobalSearchScreen(
GlobalSearchScreen(
state = state,
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(BrowseSourceScreen(it.id, state.searchQuery))
},
@@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import eu.kanade.tachiyomi.source.Source
class GlobalSearchScreenModel(
initialQuery: String = "",
initialExtensionFilter: String? = null,
) : SearchScreenModel(State(searchQuery = initialQuery)) {
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source
class GlobalSearchViewModel(
initialQuery: String,
initialExtensionFilter: String?,
) : SearchViewModel(State(searchQuery = initialQuery)) {
companion object {
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val INITIAL_EXTENSION_FILTER_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
GlobalSearchViewModel(
initialQuery = get(INITIAL_QUERY_KEY)!!,
initialExtensionFilter = get(INITIAL_EXTENSION_FILTER_KEY),
)
}
}
}
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import androidx.lifecycle.viewModelScope
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.Job
@@ -19,6 +17,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle
import tachiyomi.core.common.util.lang.launchIO
@@ -30,7 +29,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.Executors
abstract class SearchScreenModel(
abstract class SearchViewModel(
initialState: State = State(),
sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
@@ -38,7 +37,7 @@ abstract class SearchScreenModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<SearchScreenModel.State>(initialState) {
) : StateViewModel<SearchViewModel.State>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
private var searchJob: Job? = null
@@ -61,7 +60,7 @@ abstract class SearchScreenModel(
}
init {
screenModelScope.launch {
viewModelScope.launch {
preferences.globalSearchFilterState.changes().collectLatest { state ->
mutableState.update { it.copy(onlyShowHasResults = state) }
}
@@ -147,7 +146,7 @@ abstract class SearchScreenModel(
)
}
searchJob = ioCoroutineScope.launch {
searchJob = viewModelScope.launchIO {
sources.map { source ->
async {
if (state.value.items[source] !is SearchItemResult.Loading) {
@@ -192,7 +191,7 @@ abstract class SearchScreenModel(
}
fun setMigrateDialog(currentId: Long, target: Manga) {
screenModelScope.launchIO {
viewModelScope.launchIO {
val current = getManga.await(currentId) ?: return@launchIO
mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) }
}
@@ -6,7 +6,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastMap
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.category.CategoryScreen
@@ -24,9 +24,9 @@ class CategoryScreen : Screen() {
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { CategoryScreenModel() }
val viewModel = viewModel<CategoryViewModel>()
val state by screenModel.state.collectAsState()
val state by viewModel.state.collectAsState()
if (state is CategoryScreenState.Loading) {
LoadingScreen()
@@ -37,10 +37,10 @@ class CategoryScreen : Screen() {
CategoryScreen(
state = successState,
onClickCreate = { screenModel.showDialog(CategoryDialog.Create) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
onChangeOrder = screenModel::changeOrder,
onClickCreate = { viewModel.showDialog(CategoryDialog.Create) },
onClickRename = { viewModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { viewModel.showDialog(CategoryDialog.Delete(it)) },
onChangeOrder = viewModel::changeOrder,
navigateUp = navigator::pop,
)
@@ -48,30 +48,30 @@ class CategoryScreen : Screen() {
null -> {}
CategoryDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = screenModel::createCategory,
onDismissRequest = viewModel::dismissDialog,
onCreate = viewModel::createCategory,
categories = successState.categories.fastMap { it.name },
)
}
is CategoryDialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
onDismissRequest = viewModel::dismissDialog,
onRename = { viewModel.renameCategory(dialog.category, it) },
categories = successState.categories.fastMap { it.name },
category = dialog.category.name,
)
}
is CategoryDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) },
onDismissRequest = viewModel::dismissDialog,
onDelete = { viewModel.deleteCategory(dialog.category.id) },
category = dialog.category.name,
)
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
viewModel.events.collectLatest { event ->
if (event is CategoryEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.category
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 kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import mihon.core.viewmodel.StateViewModel
import tachiyomi.domain.category.interactor.CreateCategoryWithName
import tachiyomi.domain.category.interactor.DeleteCategory
import tachiyomi.domain.category.interactor.GetCategories
@@ -19,19 +19,19 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CategoryScreenModel(
class CategoryViewModel(
private val getCategories: GetCategories = Injekt.get(),
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
private val deleteCategory: DeleteCategory = Injekt.get(),
private val reorderCategory: ReorderCategory = Injekt.get(),
private val renameCategory: RenameCategory = Injekt.get(),
) : StateScreenModel<CategoryScreenState>(CategoryScreenState.Loading) {
) : StateViewModel<CategoryScreenState>(CategoryScreenState.Loading) {
private val _events: Channel<CategoryEvent> = Channel()
val events = _events.receiveAsFlow()
init {
screenModelScope.launch {
viewModelScope.launch {
getCategories.subscribe()
.collectLatest { categories ->
mutableState.update {
@@ -45,7 +45,7 @@ class CategoryScreenModel(
}
fun createCategory(name: String) {
screenModelScope.launch {
viewModelScope.launch {
when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -54,7 +54,7 @@ class CategoryScreenModel(
}
fun deleteCategory(categoryId: Long) {
screenModelScope.launch {
viewModelScope.launch {
when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -63,7 +63,7 @@ class CategoryScreenModel(
}
fun changeOrder(category: Category, newIndex: Int) {
screenModelScope.launch {
viewModelScope.launch {
when (reorderCategory.await(category, newIndex)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -72,7 +72,7 @@ class CategoryScreenModel(
}
fun renameCategory(category: Category, name: String) {
screenModelScope.launch {
viewModelScope.launch {
when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -6,7 +6,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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
@@ -28,10 +29,13 @@ class DeepLinkScreen(
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel {
DeepLinkScreenModel(query = query)
}
val state by screenModel.state.collectAsState()
val viewModel = viewModel<DeepLinkViewModel>(
factory = DeepLinkViewModel.Factory,
extras = CreationExtras {
set(DeepLinkViewModel.QUERY_KEY, query)
},
)
val state by viewModel.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@@ -42,14 +46,14 @@ class DeepLinkScreen(
},
) { contentPadding ->
when (state) {
is DeepLinkScreenModel.State.Loading -> {
is DeepLinkViewModel.State.Loading -> {
LoadingScreen(Modifier.padding(contentPadding))
}
is DeepLinkScreenModel.State.NoResults -> {
is DeepLinkViewModel.State.NoResults -> {
navigator.replace(GlobalSearchScreen(query))
}
is DeepLinkScreenModel.State.Result -> {
val resultState = state as DeepLinkScreenModel.State.Result
is DeepLinkViewModel.State.Result -> {
val resultState = state as DeepLinkViewModel.State.Result
if (resultState.chapterId == null) {
navigator.replace(
MangaScreen(
@@ -1,13 +1,16 @@
package eu.kanade.tachiyomi.ui.deeplink
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 eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update
import mihon.core.viewmodel.StateViewModel
import mihon.domain.manga.model.toDomainManga
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.launchIO
@@ -19,16 +22,28 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class DeepLinkScreenModel(
query: String = "",
class DeepLinkViewModel(
query: String,
private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
) : StateViewModel<DeepLinkViewModel.State>(State.Loading) {
companion object {
val QUERY_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
DeepLinkViewModel(
query = get(QUERY_KEY)!!,
)
}
}
}
init {
screenModelScope.launchIO {
viewModelScope.launchIO {
val source = sourceManager.getAll()
.filterIsInstance<ResolvableSource>()
.firstOrNull { it.getUriType(query) != UriType.Unknown }
@@ -41,8 +41,8 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
@@ -65,8 +65,8 @@ object DownloadQueueScreen : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val screenModel = rememberScreenModel { DownloadQueueScreenModel() }
val downloadList by screenModel.state.collectAsState()
val viewModel = viewModel<DownloadQueueViewModel>()
val downloadList by viewModel.state.collectAsState()
val downloadCount by remember {
derivedStateOf { downloadList.sumOf { it.subItems.size } }
}
@@ -133,7 +133,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_newest)) },
onClick = {
screenModel.reorderQueue(
viewModel.reorderQueue(
{ it.download.chapter.dateUpload },
true,
)
@@ -143,7 +143,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_oldest)) },
onClick = {
screenModel.reorderQueue(
viewModel.reorderQueue(
{ it.download.chapter.dateUpload },
false,
)
@@ -158,7 +158,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_asc)) },
onClick = {
screenModel.reorderQueue(
viewModel.reorderQueue(
{ it.download.chapter.chapterNumber },
false,
)
@@ -168,7 +168,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_desc)) },
onClick = {
screenModel.reorderQueue(
viewModel.reorderQueue(
{ it.download.chapter.chapterNumber },
true,
)
@@ -188,7 +188,7 @@ object DownloadQueueScreen : Screen() {
),
AppBar.OverflowAction(
title = stringResource(MR.strings.action_cancel_all),
onClick = { screenModel.clearQueue() },
onClick = { viewModel.clearQueue() },
),
),
)
@@ -198,7 +198,7 @@ object DownloadQueueScreen : Screen() {
)
},
floatingActionButton = {
val isRunning by screenModel.isDownloaderRunning.collectAsState()
val isRunning by viewModel.isDownloaderRunning.collectAsState()
SmallExtendedFloatingActionButton(
text = {
val id = if (isRunning) {
@@ -218,9 +218,9 @@ object DownloadQueueScreen : Screen() {
},
onClick = {
if (isRunning) {
screenModel.pauseDownloads()
viewModel.pauseDownloads()
} else {
screenModel.startDownloads()
viewModel.startDownloads()
}
},
expanded = fabExpanded,
@@ -250,27 +250,27 @@ object DownloadQueueScreen : Screen() {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter = DownloadAdapter(screenModel.listener)
screenModel.controllerBinding.root.adapter = screenModel.adapter
screenModel.adapter?.isHandleDragEnabled = true
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
viewModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
viewModel.adapter = DownloadAdapter(viewModel.listener)
viewModel.controllerBinding.root.adapter = viewModel.adapter
viewModel.adapter?.isHandleDragEnabled = true
viewModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
ViewCompat.setNestedScrollingEnabled(viewModel.controllerBinding.root, true)
scope.launchUI {
screenModel.getDownloadStatusFlow()
.collect(screenModel::onStatusChange)
viewModel.getDownloadStatusFlow()
.collect(viewModel::onStatusChange)
}
scope.launchUI {
screenModel.getDownloadProgressFlow()
.collect(screenModel::onUpdateDownloadedPages)
viewModel.getDownloadProgressFlow()
.collect(viewModel::onUpdateDownloadedPages)
}
screenModel.controllerBinding.root
viewModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.root
viewModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
@@ -278,7 +278,7 @@ object DownloadQueueScreen : Screen() {
bottom = bottom,
)
screenModel.adapter?.updateDataSet(downloadList)
viewModel.adapter?.updateDataSet(downloadList)
},
)
}
@@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
@@ -25,9 +25,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.milliseconds
class DownloadQueueScreenModel(
class DownloadQueueViewModel(
private val downloadManager: DownloadManager = Injekt.get(),
) : ScreenModel {
) : ViewModel() {
private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
val state = _state.asStateFlow()
@@ -117,7 +117,7 @@ class DownloadQueueScreenModel(
}
init {
screenModelScope.launch {
viewModelScope.launch {
downloadManager.queueState
.map { downloads ->
downloads
@@ -132,7 +132,7 @@ class DownloadQueueScreenModel(
}
}
override fun onDispose() {
override fun onCleared() {
for (job in progressJobs.values) {
job.cancel()
}
@@ -141,7 +141,7 @@ class DownloadQueueScreenModel(
}
val isDownloaderRunning = downloadManager.isDownloaderRunning
.stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
fun getDownloadStatusFlow() = downloadManager.statusFlow()
fun getDownloadProgressFlow() = downloadManager.progressFlow()
@@ -211,7 +211,7 @@ class DownloadQueueScreenModel(
* @param download the download to observe its progress.
*/
private fun launchProgressJob(download: Download) {
val job = screenModelScope.launch {
val job = viewModelScope.launch {
while (download.pages == null) {
delay(50.milliseconds)
}
@@ -10,7 +10,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.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
@@ -62,59 +62,59 @@ data object HistoryTab : Tab {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val screenModel = rememberScreenModel { HistoryScreenModel() }
val state by screenModel.state.collectAsState()
val viewModel = viewModel<HistoryViewModel>()
val state by viewModel.state.collectAsState()
HistoryScreen(
state = state,
snackbarHostState = snackbarHostState,
onSearchQueryChange = screenModel::updateSearchQuery,
onSearchQueryChange = viewModel::updateSearchQuery,
onClickCover = { navigator.push(MangaScreen(it)) },
onClickResume = screenModel::getNextChapterForManga,
onDialogChange = screenModel::setDialog,
onClickFavorite = screenModel::addFavorite,
onClickResume = viewModel::getNextChapterForManga,
onDialogChange = viewModel::setDialog,
onClickFavorite = viewModel::addFavorite,
)
val onDismissRequest = { screenModel.setDialog(null) }
val onDismissRequest = { viewModel.setDialog(null) }
when (val dialog = state.dialog) {
is HistoryScreenModel.Dialog.Delete -> {
is HistoryViewModel.Dialog.Delete -> {
HistoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { all ->
if (all) {
screenModel.removeAllFromHistory(dialog.history.mangaId)
viewModel.removeAllFromHistory(dialog.history.mangaId)
} else {
screenModel.removeFromHistory(dialog.history)
viewModel.removeFromHistory(dialog.history)
}
},
)
}
is HistoryScreenModel.Dialog.DeleteAll -> {
is HistoryViewModel.Dialog.DeleteAll -> {
HistoryDeleteAllDialog(
onDismissRequest = onDismissRequest,
onDelete = screenModel::removeAllHistory,
onDelete = viewModel::removeAllHistory,
)
}
is HistoryScreenModel.Dialog.DuplicateManga -> {
is HistoryViewModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onConfirm = { viewModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
onMigrate = { viewModel.showMigrateDialog(dialog.manga, it) },
)
}
is HistoryScreenModel.Dialog.ChangeCategory -> {
is HistoryViewModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
viewModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
},
)
}
is HistoryScreenModel.Dialog.Migrate -> {
is HistoryViewModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
@@ -133,20 +133,20 @@ data object HistoryTab : Tab {
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { e ->
viewModel.events.collectLatest { e ->
when (e) {
HistoryScreenModel.Event.InternalError ->
HistoryViewModel.Event.InternalError ->
snackbarHostState.showSnackbar(context.stringResource(MR.strings.internal_error))
HistoryScreenModel.Event.HistoryCleared ->
HistoryViewModel.Event.HistoryCleared ->
snackbarHostState.showSnackbar(context.stringResource(MR.strings.clear_history_completed))
is HistoryScreenModel.Event.OpenChapter -> openChapter(context, e.chapter)
is HistoryViewModel.Event.OpenChapter -> openChapter(context, e.chapter)
}
}
}
LaunchedEffect(Unit) {
resumeLastChapterReadEvent.receiveAsFlow().collectLatest {
openChapter(context, screenModel.getNextChapter())
openChapter(context, viewModel.getNextChapter())
}
}
}

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