mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-18 06:32:20 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0648e2eaaf | ||
|
|
7d4e1476ba | ||
|
+9 |
46a2471abb | ||
|
|
f63dc799f0 | ||
|
|
c114028a33 | ||
|
|
4296d056b5 | ||
|
|
7aaa891aef | ||
|
|
2ccb0ca163 | ||
|
|
44cd9a55d1 | ||
|
|
2a92f0b46a | ||
|
|
f37e81991e | ||
|
|
d15a05602a | ||
|
|
fbf5ae82f7 | ||
|
|
fdb223d657 | ||
|
|
54ae69eaa2 | ||
|
|
45b1e781eb | ||
|
|
069e5c91ac | ||
|
|
4ac9f3474b | ||
|
|
9c3cc1ca5f | ||
|
|
47d7c0f2ea | ||
|
|
0b5e9bb815 | ||
|
|
d72ae365cb | ||
|
|
e7ec672c8b | ||
|
|
628cb58862 | ||
|
|
e454cf8e9d | ||
|
|
baa0fb7ecb | ||
|
|
2d2cf0879f | ||
|
|
6981185f4b | ||
|
|
47aa9190d4 | ||
|
|
98705910e7 | ||
|
|
5ce7d00eb8 | ||
|
|
03229a3807 | ||
|
|
493140c7ff | ||
|
|
98bb731b4d | ||
|
|
c3b99aea07 | ||
|
|
ef1c529672 | ||
|
|
633eea4ced | ||
|
|
31a4d6ba39 | ||
|
|
e9b7c760a1 | ||
|
|
ef1e7ba907 | ||
|
|
526d79a975 | ||
|
|
b9ac9a0422 | ||
|
|
d171768cff | ||
|
|
cb2617dee0 | ||
|
|
b4635c41a8 | ||
|
|
217b20123c | ||
|
|
b2ca6bc5f3 |
@@ -30,7 +30,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.20.1](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
+9
-9
@@ -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)
|
||||
}
|
||||
|
||||
+18
-18
@@ -30,9 +30,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
@@ -42,6 +41,7 @@ import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.core.common.util.lang.toLong
|
||||
@@ -67,13 +67,13 @@ class ClearDatabaseScreen : Screen() {
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { ClearDatabaseScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<ClearDatabaseViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
when (val s = state) {
|
||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseScreenModel.State.Ready -> {
|
||||
is ClearDatabaseViewModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseViewModel.State.Ready -> {
|
||||
if (s.showConfirmation) {
|
||||
var keepReadManga by remember { mutableStateOf(true) }
|
||||
AlertDialog(
|
||||
@@ -108,14 +108,14 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = model::hideConfirmation,
|
||||
onDismissRequest = viewModel::hideConfirmation,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launchUI {
|
||||
model.removeMangaBySourceId(keepReadManga)
|
||||
model.clearSelection()
|
||||
model.hideConfirmation()
|
||||
viewModel.removeMangaBySourceId(keepReadManga)
|
||||
viewModel.clearSelection()
|
||||
viewModel.hideConfirmation()
|
||||
context.toast(MR.strings.clear_database_completed)
|
||||
}
|
||||
},
|
||||
@@ -124,7 +124,7 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = model::hideConfirmation) {
|
||||
TextButton(onClick = viewModel::hideConfirmation) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
@@ -143,12 +143,12 @@ class ClearDatabaseScreen : Screen() {
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = model::selectAll,
|
||||
onClick = viewModel::selectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = model::invertSelection,
|
||||
onClick = viewModel::invertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -168,14 +168,14 @@ class ClearDatabaseScreen : Screen() {
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_delete),
|
||||
actionEnabled = s.selection.isNotEmpty(),
|
||||
onClickAction = model::showConfirmation,
|
||||
onClickAction = viewModel::showConfirmation,
|
||||
) {
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
source = sourceWithCount.source,
|
||||
count = sourceWithCount.count,
|
||||
isSelected = s.selection.contains(sourceWithCount.id),
|
||||
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
|
||||
onClickSelect = { viewModel.toggleSelection(sourceWithCount.source) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -220,12 +220,12 @@ class ClearDatabaseScreen : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {
|
||||
class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(State.Loading) {
|
||||
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
|
||||
private val database: Database = Injekt.get()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
getSourcesWithNonLibraryManga.subscribe()
|
||||
.collectLatest { list ->
|
||||
mutableState.update { old ->
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+13
-13
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog
|
||||
@@ -26,11 +26,11 @@ class ExtensionStoresScreen(
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
val viewModel = viewModel<ExtensionStoresViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(url) {
|
||||
url?.let { screenModel.addFromDeeplink(url) }
|
||||
url?.let { viewModel.addFromDeeplink(url) }
|
||||
}
|
||||
|
||||
if (state is ExtensionStoreScreenState.Loading) {
|
||||
@@ -42,12 +42,12 @@ class ExtensionStoresScreen(
|
||||
|
||||
ExtensionStoresScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||
onClickCreate = { viewModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
|
||||
onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
|
||||
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
|
||||
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||
onClickRefresh = { screenModel.refreshRepos() },
|
||||
onClickDelete = { viewModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||
onClickRefresh = { viewModel.refreshRepos() },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
@@ -55,8 +55,8 @@ class ExtensionStoresScreen(
|
||||
null -> {}
|
||||
is ExtensionStoreDialog.Create -> {
|
||||
ExtensionStoreCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onCreate = { viewModel.createRepo(it) },
|
||||
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
|
||||
processing = dialog.processing,
|
||||
errorMessage = dialog.errorMessage,
|
||||
@@ -64,16 +64,16 @@ class ExtensionStoresScreen(
|
||||
}
|
||||
is ExtensionStoreDialog.Delete -> {
|
||||
ExtensionStoreDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onDelete = { viewModel.deleteRepo(dialog.store.indexUrl) },
|
||||
storeName = dialog.store.name,
|
||||
storeIndexUrl = dialog.store.indexUrl,
|
||||
)
|
||||
}
|
||||
is ExtensionStoreDialog.Confirm -> {
|
||||
ExtensionStoreConfirmDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(dialog.url) },
|
||||
onDismissRequest = viewModel::dismissDialog,
|
||||
onCreate = { viewModel.createRepo(dialog.url) },
|
||||
storeIndexUrl = dialog.url,
|
||||
storeAlreadyExists = dialog.alreadyExists,
|
||||
processing = dialog.processing,
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import mihon.domain.extension.interactor.AddExtensionStore
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||
@@ -16,13 +16,13 @@ import tachiyomi.core.common.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionStoresScreenModel(
|
||||
class ExtensionStoresViewModel(
|
||||
private val getExtensionStores: GetExtensionStores = Injekt.get(),
|
||||
private val addExtensionStore: AddExtensionStore = Injekt.get(),
|
||||
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
|
||||
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||
) : StateViewModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||
|
||||
private inline fun updateSuccessState(
|
||||
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
|
||||
@@ -36,7 +36,7 @@ class ExtensionStoresScreenModel(
|
||||
}
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
getExtensionStores.subscribe()
|
||||
.collectLatest { stores ->
|
||||
mutableState.update {
|
||||
@@ -55,7 +55,7 @@ class ExtensionStoresScreenModel(
|
||||
* @param baseUrl The baseUrl of the repo to create.
|
||||
*/
|
||||
fun createRepo(baseUrl: String) {
|
||||
screenModelScope.launch {
|
||||
viewModelScope.launch {
|
||||
updateSuccessState {
|
||||
it.copy(
|
||||
dialog = when (it.dialog) {
|
||||
@@ -97,7 +97,7 @@ class ExtensionStoresScreenModel(
|
||||
val status = state.value
|
||||
|
||||
if (status is ExtensionStoreScreenState.Success) {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
updateExtensionStores()
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ class ExtensionStoresScreenModel(
|
||||
* Deletes the given repo from the database
|
||||
*/
|
||||
fun deleteRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
viewModelScope.launchIO {
|
||||
removeExtensionStore(baseUrl)
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
-4
@@ -20,10 +20,8 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoreCreateDialog(
|
||||
@@ -94,8 +92,6 @@ fun ExtensionStoreCreateDialog(
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -11,8 +11,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
@@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
@@ -37,8 +37,8 @@ class CreateBackupScreen : Screen() {
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { CreateBackupScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<CreateBackupViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val chooseBackupDir = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/*"),
|
||||
@@ -49,7 +49,7 @@ class CreateBackupScreen : Screen() {
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
model.createBackup(context, it)
|
||||
viewModel.createBackup(context, it)
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
@@ -87,13 +87,13 @@ class CreateBackupScreen : Screen() {
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_library) {
|
||||
Options(BackupOptions.libraryOptions, state, model)
|
||||
Options(BackupOptions.libraryOptions, state, viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_settings) {
|
||||
Options(BackupOptions.settingsOptions, state, model)
|
||||
Options(BackupOptions.settingsOptions, state, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +103,8 @@ class CreateBackupScreen : Screen() {
|
||||
@Composable
|
||||
private fun Options(
|
||||
options: List<BackupOptions.Entry>,
|
||||
state: CreateBackupScreenModel.State,
|
||||
model: CreateBackupScreenModel,
|
||||
state: CreateBackupViewModel.State,
|
||||
model: CreateBackupViewModel,
|
||||
) {
|
||||
options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
@@ -119,7 +119,7 @@ class CreateBackupScreen : Screen() {
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||
class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) {
|
||||
|
||||
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
|
||||
+32
-10
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.more.settings.screen.data
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -14,14 +15,15 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
@@ -32,6 +34,7 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||
@@ -39,6 +42,8 @@ import tachiyomi.presentation.core.components.SectionCard
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class RestoreBackupScreen(
|
||||
private val uri: String,
|
||||
@@ -46,10 +51,14 @@ class RestoreBackupScreen(
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
|
||||
val state by model.state.collectAsState()
|
||||
val viewModel = viewModel<RestoreBackupViewModel>(
|
||||
factory = RestoreBackupViewModel.Factory,
|
||||
extras = CreationExtras {
|
||||
set(RestoreBackupViewModel.URI_KEY, uri)
|
||||
},
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -65,7 +74,7 @@ class RestoreBackupScreen(
|
||||
actionLabel = stringResource(MR.strings.action_restore),
|
||||
actionEnabled = state.canRestore && state.options.canRestore(),
|
||||
onClickAction = {
|
||||
model.startRestore()
|
||||
viewModel.startRestore()
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
@@ -83,7 +92,7 @@ class RestoreBackupScreen(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
viewModel.toggle(option.setter, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -164,10 +173,23 @@ class RestoreBackupScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private class RestoreBackupScreenModel(
|
||||
class RestoreBackupViewModel(
|
||||
private val context: Context,
|
||||
private val uri: String,
|
||||
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||
) : StateViewModel<RestoreBackupViewModel.State>(State()) {
|
||||
|
||||
companion object {
|
||||
val URI_KEY = CreationExtras.Key<String>()
|
||||
|
||||
val Factory = viewModelFactory {
|
||||
initializer {
|
||||
RestoreBackupViewModel(
|
||||
context = Injekt.get<Application>(),
|
||||
uri = get(URI_KEY)!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
validate(uri.toUri())
|
||||
|
||||
+27
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -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)
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+13
-13
@@ -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)
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -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()
|
||||
}
|
||||
|
||||
+22
-5
@@ -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 } }
|
||||
+15
-9
@@ -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,
|
||||
) {
|
||||
|
||||
+20
-5
@@ -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))
|
||||
}
|
||||
+18
-12
@@ -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
|
||||
|
||||
+20
-5
@@ -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(
|
||||
+26
-19
@@ -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,
|
||||
|
||||
+5
-5
@@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
+7
-7
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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)
|
||||
+53
-46
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-12
@@ -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(),
|
||||
+15
-13
@@ -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))
|
||||
},
|
||||
|
||||
-25
@@ -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 }
|
||||
}
|
||||
}
|
||||
+42
@@ -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 }
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -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)
|
||||
}
|
||||
|
||||
+9
-9
@@ -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(
|
||||
|
||||
+21
-6
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+8
-8
@@ -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
Reference in New Issue
Block a user