Compare commits

..
1 Commits
Author SHA1 Message Date
AntsyLich fd95fb8103 Add fastlane metadata (#3564) 2026-07-11 20:04:06 +06:00
262 changed files with 5052 additions and 5256 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.20.2](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.20.1](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+2 -2
View File
@@ -52,7 +52,7 @@ body:
label: Mihon version
description: You can find your Mihon version in **More → About**.
placeholder: |
Example: "0.20.2"
Example: "0.20.1"
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.2](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.20.1](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
+2 -2
View File
@@ -27,14 +27,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- 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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
java-version-file: .github/.java-version
distribution: temurin
+5 -5
View File
@@ -32,10 +32,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up JDK
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
java-version-file: .github/.java-version
distribution: temurin
@@ -80,10 +80,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up JDK
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.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@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
tag_name: ${{ needs.get_tag.outputs.tag }}
name: Mihon ${{ needs.get_tag.outputs.tag }}
+1 -24
View File
@@ -11,31 +11,9 @@ 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
@@ -601,8 +579,7 @@ 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.2...main
[v0.20.2]: https://github.com/mihonapp/mihon/compare/v0.20.1...v0.20.2
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.20.1...main
[v0.20.1]: https://github.com/mihonapp/mihon/compare/v0.20.0...v0.20.1
[v0.20.0]: https://github.com/mihonapp/mihon/compare/v0.19.9...v0.20.0
[v0.19.9]: https://github.com/mihonapp/mihon/compare/v0.19.8...v0.19.9
+1 -1
View File
@@ -29,7 +29,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
* Local reading of content.
* A configurable reader with multiple viewers, reading directions and other settings.
* Tracker support: [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.
* 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.
* Categories to organize your library.
* Light and dark themes.
* Schedule updating your library for new chapters.
+2 -3
View File
@@ -32,8 +32,8 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 27
versionName = "0.20.2"
versionCode = 26
versionName = "0.20.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getLatestCommitSha()}\"")
@@ -209,7 +209,6 @@ 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, in IntentSender intentSender) = 1;
void install(in AssetFileDescriptor apk) = 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()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
@@ -17,7 +17,6 @@ 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.ExtensionDetailsViewModel
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
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: ExtensionDetailsViewModel.State,
state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
@@ -48,11 +48,12 @@ 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.ExtensionsViewModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
@@ -70,7 +71,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun ExtensionScreen(
state: ExtensionsViewModel.State,
state: ExtensionsScreenModel.State,
contentPadding: PaddingValues,
searchQuery: String?,
onLongClickItem: (Extension) -> Unit,
@@ -132,7 +133,7 @@ fun ExtensionScreen(
@Composable
private fun ExtensionContent(
state: ExtensionsViewModel.State,
state: ExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
@@ -186,14 +187,14 @@ private fun ExtensionContent(
}
ExtensionHeader(
textRes = header.textRes,
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
action = action,
)
}
is ExtensionUiModel.Header.Text -> {
ExtensionHeader(
text = header.text,
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
)
}
}
@@ -211,7 +212,7 @@ private fun ExtensionContent(
},
) { item ->
ExtensionItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
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.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
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: SearchViewModel.State,
state: SearchScreenModel.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.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
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: SearchViewModel.State,
state: SearchScreenModel.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.MigrateSourceViewModel
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
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: MigrateSourceViewModel.State,
state: MigrateSourceScreenModel.State,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit,
@@ -10,7 +10,8 @@ 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.tachiyomi.ui.browse.source.SourcesFilterViewModel
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
@@ -22,7 +23,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
@Composable
fun SourcesFilterScreen(
navigateUp: () -> Unit,
state: SourcesFilterViewModel.State.Success,
state: SourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
@@ -54,7 +55,7 @@ fun SourcesFilterScreen(
@Composable
private fun SourcesFilterContent(
contentPadding: PaddingValues,
state: SourcesFilterViewModel.State.Success,
state: SourcesFilterScreenModel.State.Success,
onClickLanguage: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
@@ -68,7 +69,7 @@ private fun SourcesFilterContent(
contentType = "source-filter-header",
) {
SourcesFilterHeader(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
language = language,
enabled = enabled,
onClickItem = onClickLanguage,
@@ -81,7 +82,7 @@ private fun SourcesFilterContent(
contentType = { "source-filter-item" },
) { source ->
SourcesFilterItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
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.SourcesViewModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.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: SourcesViewModel.State,
state: SourcesScreenModel.State,
contentPadding: PaddingValues,
onClickItem: (Source, Listing) -> Unit,
onClickPin: (Source) -> Unit,
@@ -27,11 +27,13 @@ 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(
@@ -89,6 +91,8 @@ fun CategoryCreateDialog(
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(0.1.seconds)
focusRequester.requestFocus()
}
}
@@ -151,6 +155,8 @@ fun CategoryRenameDialog(
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(0.1.seconds)
focusRequester.requestFocus()
}
}
@@ -58,6 +58,7 @@ fun TabbedDialog(
modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
divider = {},
) {
tabTitles.fastForEachIndexed { index, tab ->
Tab(
@@ -71,6 +72,7 @@ fun TabbedDialog(
tabOverflowMenuContent?.let { MoreMenu(it) }
}
HorizontalDivider()
HorizontalPager(
modifier = Modifier.animateContentSize(),
@@ -18,7 +18,8 @@ import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@@ -31,13 +32,13 @@ import java.time.LocalDate
@Composable
fun HistoryScreen(
state: HistoryViewModel.State,
state: HistoryScreenModel.State,
snackbarHostState: SnackbarHostState,
onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryViewModel.Dialog?) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) {
Scaffold(
topBar = { scrollBehavior ->
@@ -52,7 +53,7 @@ fun HistoryScreen(
title = stringResource(MR.strings.pref_clear_history),
icon = Icons.Outlined.DeleteSweep,
onClick = {
onDialogChange(HistoryViewModel.Dialog.DeleteAll)
onDialogChange(HistoryScreenModel.Dialog.DeleteAll)
},
),
),
@@ -82,7 +83,7 @@ fun HistoryScreen(
contentPadding = contentPadding,
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryViewModel.Dialog.Delete(item)) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
)
}
@@ -115,14 +116,14 @@ private fun HistoryScreenContent(
when (item) {
is HistoryUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
text = relativeDateText(item.date),
)
}
is HistoryUiModel.Item -> {
val value = item.item
HistoryItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
history = value,
onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) },
@@ -143,8 +144,8 @@ sealed interface HistoryUiModel {
@PreviewLightDark
@Composable
internal fun HistoryScreenPreviews(
@PreviewParameter(HistoryviewModelStateProvider::class)
historyState: HistoryViewModel.State,
@PreviewParameter(HistoryScreenModelStateProvider::class)
historyState: HistoryScreenModel.State,
) {
TachiyomiPreviewTheme {
HistoryScreen(
@@ -1,7 +1,7 @@
package eu.kanade.presentation.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.ui.history.HistoryViewModel
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
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 HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.State> {
class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenModel.State> {
private val multiPage = HistoryViewModel.State(
private val multiPage = HistoryScreenModel.State(
searchQuery = null,
list =
listOf(HistoryUiModelExamples.headerToday)
@@ -26,7 +26,7 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
dialog = null,
)
private val shortRecent = HistoryViewModel.State(
private val shortRecent = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerToday,
@@ -35,7 +35,7 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
dialog = null,
)
private val shortFuture = HistoryViewModel.State(
private val shortFuture = HistoryScreenModel.State(
searchQuery = null,
list = listOf(
HistoryUiModelExamples.headerTomorrow,
@@ -44,23 +44,23 @@ class HistoryviewModelStateProvider : PreviewParameterProvider<HistoryViewModel.
dialog = null,
)
private val empty = HistoryViewModel.State(
private val empty = HistoryScreenModel.State(
searchQuery = null,
list = listOf(),
dialog = null,
)
private val loadingWithSearchQuery = HistoryViewModel.State(
private val loadingWithSearchQuery = HistoryScreenModel.State(
searchQuery = "Example Search Query",
)
private val loading = HistoryViewModel.State(
private val loading = HistoryScreenModel.State(
searchQuery = null,
list = null,
dialog = null,
)
override val values: Sequence<HistoryViewModel.State> = sequenceOf(
override val values: Sequence<HistoryScreenModel.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.LibrarySettingsViewModel
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
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,
viewModel: LibrarySettingsViewModel,
screenModel: LibrarySettingsScreenModel,
category: Category?,
) {
TabbedDialog(
@@ -59,14 +59,14 @@ fun LibrarySettingsDialog(
) {
when (page) {
0 -> FilterPage(
viewModel = viewModel,
screenModel = screenModel,
)
1 -> SortPage(
category = category,
viewModel = viewModel,
screenModel = screenModel,
)
2 -> DisplayPage(
viewModel = viewModel,
screenModel = screenModel,
)
}
}
@@ -75,11 +75,11 @@ fun LibrarySettingsDialog(
@Composable
private fun ColumnScope.FilterPage(
viewModel: LibrarySettingsViewModel,
screenModel: LibrarySettingsScreenModel,
) {
val filterDownloaded by viewModel.libraryPreferences.filterDownloaded.collectAsState()
val downloadedOnly by viewModel.preferences.downloadedOnly.collectAsState()
val autoUpdateMangaRestrictions by viewModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded.collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly.collectAsState()
val autoUpdateMangaRestrictions by screenModel.libraryPreferences.autoUpdateMangaRestrictions.collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
@@ -89,64 +89,64 @@ private fun ColumnScope.FilterPage(
filterDownloaded
},
enabled = !downloadedOnly,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterDownloaded) },
onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) },
)
val filterUnread by viewModel.libraryPreferences.filterUnread.collectAsState()
val filterUnread by screenModel.libraryPreferences.filterUnread.collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_unread),
state = filterUnread,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterUnread) },
onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
)
val filterStarted by viewModel.libraryPreferences.filterStarted.collectAsState()
val filterStarted by screenModel.libraryPreferences.filterStarted.collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_started),
state = filterStarted,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterStarted) },
onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
)
val filterBookmarked by viewModel.libraryPreferences.filterBookmarked.collectAsState()
val filterBookmarked by screenModel.libraryPreferences.filterBookmarked.collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked),
state = filterBookmarked,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterBookmarked) },
onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
)
val filterCompleted by viewModel.libraryPreferences.filterCompleted.collectAsState()
val filterCompleted by screenModel.libraryPreferences.filterCompleted.collectAsState()
TriStateItem(
label = stringResource(MR.strings.completed),
state = filterCompleted,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterCompleted) },
onClick = { screenModel.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 viewModel.libraryPreferences.filterIntervalCustom.collectAsState()
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom.collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom),
state = filterIntervalCustom,
onClick = { viewModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
onClick = { screenModel.toggleFilter(LibraryPreferences::filterIntervalCustom) },
)
}
val trackers by viewModel.trackersFlow.collectAsState()
val trackers by screenModel.trackersFlow.collectAsState()
when (trackers.size) {
0 -> {
// No trackers
}
1 -> {
val service = trackers[0]
val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_tracked),
state = filterTracker,
onClick = { viewModel.toggleTracker(service.id.toInt()) },
onClick = { screenModel.toggleTracker(service.id.toInt()) },
)
}
else -> {
HeadingItem(MR.strings.action_filter_tracked)
trackers.map { service ->
val filterTracker by viewModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem(
label = service.name,
state = filterTracker,
onClick = { viewModel.toggleTracker(service.id.toInt()) },
onClick = { screenModel.toggleTracker(service.id.toInt()) },
)
}
}
@@ -156,9 +156,9 @@ private fun ColumnScope.FilterPage(
@Composable
private fun ColumnScope.SortPage(
category: Category?,
viewModel: LibrarySettingsViewModel,
screenModel: LibrarySettingsScreenModel,
) {
val trackers by viewModel.trackersFlow.collectAsState()
val trackers by screenModel.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 = {
viewModel.setSort(category, mode, LibrarySort.Direction.Ascending)
screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
},
)
return@map
@@ -211,7 +211,7 @@ private fun ColumnScope.SortPage(
LibrarySort.Direction.Ascending
}
}
viewModel.setSort(category, mode, direction)
screenModel.setSort(category, mode, direction)
},
)
}
@@ -226,14 +226,14 @@ private val displayModes = listOf(
@Composable
private fun ColumnScope.DisplayPage(
viewModel: LibrarySettingsViewModel,
screenModel: LibrarySettingsScreenModel,
) {
val displayMode by viewModel.libraryPreferences.displayMode.collectAsState()
val displayMode by screenModel.libraryPreferences.displayMode.collectAsState()
SettingsChipRow(MR.strings.action_display_mode) {
displayModes.map { (titleRes, mode) ->
FilterChip(
selected = displayMode == mode,
onClick = { viewModel.setDisplayMode(mode) },
onClick = { screenModel.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) {
viewModel.libraryPreferences.landscapeColumns
screenModel.libraryPreferences.landscapeColumns
} else {
viewModel.libraryPreferences.portraitColumns
screenModel.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 = viewModel.libraryPreferences.downloadBadge,
pref = screenModel.libraryPreferences.downloadBadge,
)
CheckboxItem(
label = stringResource(MR.strings.action_display_unread_badge),
pref = viewModel.libraryPreferences.unreadBadge,
pref = screenModel.libraryPreferences.unreadBadge,
)
CheckboxItem(
label = stringResource(MR.strings.action_display_local_badge),
pref = viewModel.libraryPreferences.localBadge,
pref = screenModel.libraryPreferences.localBadge,
)
CheckboxItem(
label = stringResource(MR.strings.action_display_language_badge),
pref = viewModel.libraryPreferences.languageBadge,
pref = screenModel.libraryPreferences.languageBadge,
)
CheckboxItem(
label = stringResource(MR.strings.action_display_show_continue_reading_button),
pref = viewModel.libraryPreferences.showContinueReadingButton,
pref = screenModel.libraryPreferences.showContinueReadingButton,
)
HeadingItem(MR.strings.tabs_header)
CheckboxItem(
label = stringResource(MR.strings.action_display_show_tabs),
pref = viewModel.libraryPreferences.categoryTabs,
pref = screenModel.libraryPreferences.categoryTabs,
)
CheckboxItem(
label = stringResource(MR.strings.action_display_show_number_of_items),
pref = viewModel.libraryPreferences.categoryNumberOfItems,
pref = screenModel.libraryPreferences.categoryNumberOfItems,
)
}
@@ -1,11 +1,15 @@
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
@@ -18,22 +22,29 @@ internal fun LibraryTabs(
onTabItemClick: (Int) -> Unit,
) {
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
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,
)
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,
)
}
}
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.MangaViewModel
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
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: MangaViewModel.State.Success,
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
isTabletUi: Boolean,
@@ -210,7 +210,7 @@ fun MangaScreen(
@Composable
private fun MangaScreenSmallImpl(
state: MangaViewModel.State.Success,
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -452,7 +452,7 @@ private fun MangaScreenSmallImpl(
@Composable
fun MangaScreenLargeImpl(
state: MangaViewModel.State.Success,
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -68,7 +68,7 @@ fun MangaCoverDialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
decorFitsSystemWindows = false, // Doesn't work https://issuetracker.google.com/issues/246909281
),
) {
Scaffold(
@@ -13,12 +13,10 @@ 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
@@ -29,27 +27,16 @@ import tachiyomi.presentation.core.screens.InfoScreen
fun NewUpdateScreen(
versionName: String,
changelogInfo: String,
stage: NewUpdateScreenModel.Stage,
downloadProgress: () -> Int,
onOpenInBrowser: () -> Unit,
onAcceptUpdate: () -> Unit,
onRejectUpdate: () -> Unit,
onAcceptUpdate: () -> Unit,
) {
InfoScreen(
icon = Icons.Outlined.NewReleases,
headingText = stringResource(MR.strings.update_check_notification_update_available),
subtitleText = versionName,
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)
},
acceptText = stringResource(MR.strings.update_check_confirm),
onAcceptClick = onAcceptUpdate,
canAccept = stage != NewUpdateScreenModel.Stage.Downloading,
rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate,
) {
@@ -60,7 +47,7 @@ fun NewUpdateScreen(
) {
MarkdownRender(
content = changelogInfo,
flavour = remember { GFMFlavourDescriptor() },
flavour = GFMFlavourDescriptor(),
)
TextButton(
@@ -89,11 +76,9 @@ private fun NewUpdateScreenPreview() {
- Hello
- World
""".trimIndent(),
stage = NewUpdateScreenModel.Stage.Available,
downloadProgress = { 0 },
onOpenInBrowser = {},
onAcceptUpdate = {},
onRejectUpdate = {},
onAcceptUpdate = {},
)
}
}
@@ -219,7 +219,7 @@ object AboutScreen : Screen() {
val updateChecker = AppUpdateChecker()
withUIContext {
try {
when (val result = withIOContext { updateChecker.checkForUpdate(forceCheck = true) }) {
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
is GetApplicationRelease.Result.NewUpdate -> {
onAvailableUpdate(result)
}
@@ -30,8 +30,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.components.SourceIcon
@@ -41,7 +42,6 @@ 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 viewModel = viewModel<ClearDatabaseViewModel>()
val state by viewModel.state.collectAsState()
val model = rememberScreenModel { ClearDatabaseScreenModel() }
val state by model.state.collectAsState()
val scope = rememberCoroutineScope()
when (val s = state) {
is ClearDatabaseViewModel.State.Loading -> LoadingScreen()
is ClearDatabaseViewModel.State.Ready -> {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) {
var keepReadManga by remember { mutableStateOf(true) }
AlertDialog(
@@ -108,14 +108,14 @@ class ClearDatabaseScreen : Screen() {
}
}
},
onDismissRequest = viewModel::hideConfirmation,
onDismissRequest = model::hideConfirmation,
confirmButton = {
TextButton(
onClick = {
scope.launchUI {
viewModel.removeMangaBySourceId(keepReadManga)
viewModel.clearSelection()
viewModel.hideConfirmation()
model.removeMangaBySourceId(keepReadManga)
model.clearSelection()
model.hideConfirmation()
context.toast(MR.strings.clear_database_completed)
}
},
@@ -124,7 +124,7 @@ class ClearDatabaseScreen : Screen() {
}
},
dismissButton = {
TextButton(onClick = viewModel::hideConfirmation) {
TextButton(onClick = model::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 = viewModel::selectAll,
onClick = model::selectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = viewModel::invertSelection,
onClick = model::invertSelection,
),
),
)
@@ -168,14 +168,14 @@ class ClearDatabaseScreen : Screen() {
contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_delete),
actionEnabled = s.selection.isNotEmpty(),
onClickAction = viewModel::showConfirmation,
onClickAction = model::showConfirmation,
) {
items(s.items) { sourceWithCount ->
ClearDatabaseItem(
source = sourceWithCount.source,
count = sourceWithCount.count,
isSelected = s.selection.contains(sourceWithCount.id),
onClickSelect = { viewModel.toggleSelection(sourceWithCount.source) },
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
)
}
}
@@ -220,12 +220,12 @@ class ClearDatabaseScreen : Screen() {
}
}
class ClearDatabaseViewModel : StateViewModel<ClearDatabaseViewModel.State>(State.Loading) {
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
private val database: Database = Injekt.get()
init {
viewModelScope.launchIO {
screenModelScope.launchIO {
getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list ->
mutableState.update { old ->
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<ExtensionStoresViewModel>()
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
val state by screenModel.state.collectAsState()
LaunchedEffect(url) {
url?.let { viewModel.addFromDeeplink(url) }
url?.let { screenModel.addFromDeeplink(url) }
}
if (state is ExtensionStoreScreenState.Loading) {
@@ -42,12 +42,12 @@ class ExtensionStoresScreen(
ExtensionStoresScreen(
state = successState,
onClickCreate = { viewModel.showDialog(ExtensionStoreDialog.Create()) },
onClickCreate = { screenModel.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 = { viewModel.showDialog(ExtensionStoreDialog.Delete(it)) },
onClickRefresh = { viewModel.refreshRepos() },
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
onClickRefresh = { screenModel.refreshRepos() },
navigateUp = navigator::pop,
)
@@ -55,8 +55,8 @@ class ExtensionStoresScreen(
null -> {}
is ExtensionStoreDialog.Create -> {
ExtensionStoreCreateDialog(
onDismissRequest = viewModel::dismissDialog,
onCreate = { viewModel.createRepo(it) },
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.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 = viewModel::dismissDialog,
onDelete = { viewModel.deleteRepo(dialog.store.indexUrl) },
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
storeName = dialog.store.name,
storeIndexUrl = dialog.store.indexUrl,
)
}
is ExtensionStoreDialog.Confirm -> {
ExtensionStoreConfirmDialog(
onDismissRequest = viewModel::dismissDialog,
onCreate = { viewModel.createRepo(dialog.url) },
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(dialog.url) },
storeIndexUrl = dialog.url,
storeAlreadyExists = dialog.alreadyExists,
processing = dialog.processing,
@@ -1,12 +1,12 @@
package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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 ExtensionStoresViewModel(
class ExtensionStoresScreenModel(
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(),
) : StateViewModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
private inline fun updateSuccessState(
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
@@ -36,7 +36,7 @@ class ExtensionStoresViewModel(
}
init {
viewModelScope.launchIO {
screenModelScope.launchIO {
getExtensionStores.subscribe()
.collectLatest { stores ->
mutableState.update {
@@ -55,7 +55,7 @@ class ExtensionStoresViewModel(
* @param baseUrl The baseUrl of the repo to create.
*/
fun createRepo(baseUrl: String) {
viewModelScope.launch {
screenModelScope.launch {
updateSuccessState {
it.copy(
dialog = when (it.dialog) {
@@ -97,7 +97,7 @@ class ExtensionStoresViewModel(
val status = state.value
if (status is ExtensionStoreScreenState.Success) {
viewModelScope.launchIO {
screenModelScope.launchIO {
updateExtensionStores()
}
}
@@ -107,7 +107,7 @@ class ExtensionStoresViewModel(
* Deletes the given repo from the database
*/
fun deleteRepo(baseUrl: String) {
viewModelScope.launchIO {
screenModelScope.launchIO {
removeExtensionStore(baseUrl)
extensionManager.findAvailableExtensions()
}
@@ -20,8 +20,10 @@ 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(
@@ -92,6 +94,8 @@ fun ExtensionStoreCreateDialog(
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(0.1.seconds)
focusRequester.requestFocus()
}
}
@@ -11,7 +11,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.StateScreenModel
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
@@ -23,7 +24,6 @@ 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 viewModel = viewModel<CreateBackupViewModel>()
val state by viewModel.state.collectAsState()
val model = rememberScreenModel { CreateBackupScreenModel() }
val state by model.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,
)
viewModel.createBackup(context, it)
model.createBackup(context, it)
navigator.pop()
}
}
@@ -87,13 +87,13 @@ class CreateBackupScreen : Screen() {
item {
SectionCard(MR.strings.label_library) {
Options(BackupOptions.libraryOptions, state, viewModel)
Options(BackupOptions.libraryOptions, state, model)
}
}
item {
SectionCard(MR.strings.label_settings) {
Options(BackupOptions.settingsOptions, state, viewModel)
Options(BackupOptions.settingsOptions, state, model)
}
}
}
@@ -103,8 +103,8 @@ class CreateBackupScreen : Screen() {
@Composable
private fun Options(
options: List<BackupOptions.Entry>,
state: CreateBackupViewModel.State,
model: CreateBackupViewModel,
state: CreateBackupScreenModel.State,
model: CreateBackupScreenModel,
) {
options.forEach { option ->
LabeledCheckbox(
@@ -119,7 +119,7 @@ class CreateBackupScreen : Screen() {
}
}
class CreateBackupViewModel : StateViewModel<CreateBackupViewModel.State>(State()) {
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
mutableState.update {
@@ -1,6 +1,5 @@
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
@@ -15,15 +14,14 @@ 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 androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.core.model.StateScreenModel
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
@@ -34,7 +32,6 @@ 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
@@ -42,8 +39,6 @@ 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,
@@ -51,14 +46,10 @@ class RestoreBackupScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<RestoreBackupViewModel>(
factory = RestoreBackupViewModel.Factory,
extras = CreationExtras {
set(RestoreBackupViewModel.URI_KEY, uri)
},
)
val state by viewModel.state.collectAsState()
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
val state by model.state.collectAsState()
Scaffold(
topBar = {
@@ -74,7 +65,7 @@ class RestoreBackupScreen(
actionLabel = stringResource(MR.strings.action_restore),
actionEnabled = state.canRestore && state.options.canRestore(),
onClickAction = {
viewModel.startRestore()
model.startRestore()
navigator.pop()
},
) {
@@ -92,7 +83,7 @@ class RestoreBackupScreen(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
viewModel.toggle(option.setter, it)
model.toggle(option.setter, it)
},
)
}
@@ -173,23 +164,10 @@ class RestoreBackupScreen(
}
}
class RestoreBackupViewModel(
private class RestoreBackupScreenModel(
private val context: Context,
private val uri: String,
) : 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)!!,
)
}
}
}
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
init {
validate(uri.toUri())
@@ -1,6 +1,5 @@
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
@@ -19,19 +18,17 @@ 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
@@ -59,10 +56,10 @@ class WorkerInfoScreen : Screen() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
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()
val screenModel = rememberScreenModel { Model(context) }
val enqueued by screenModel.enqueued.collectAsState()
val finished by screenModel.finished.collectAsState()
val running by screenModel.running.collectAsState()
Scaffold(
topBar = {
@@ -120,20 +117,7 @@ class WorkerInfoScreen : Screen() {
)
}
class WorkerInfoViewModel(
context: Context,
) : ViewModel() {
companion object {
val Factory = viewModelFactory {
initializer {
WorkerInfoViewModel(
context = Injekt.get<Application>(),
)
}
}
}
private class Model(context: Context) : ScreenModel {
private val workManager = context.workManager
val finished = workManager
@@ -141,17 +125,17 @@ class WorkerInfoScreen : Screen() {
WorkQuery.fromStates(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED),
)
.map(::constructString)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
val running = workManager
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.map(::constructString)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
.stateIn(ioCoroutineScope, SharingStarted.WhileSubscribed(), "")
val enqueued = workManager
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
.map(::constructString)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
.stateIn(ioCoroutineScope, 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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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,
viewModel: ReaderSettingsViewModel,
screenModel: ReaderSettingsScreenModel,
onChange: (StringResource) -> Unit,
) {
val manga by viewModel.mangaFlow.collectAsState()
val manga by screenModel.mangaFlow.collectAsState()
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) {
DialogContent(
orientation = orientation,
onChangeOrientation = {
viewModel.onChangeOrientation(it)
screenModel.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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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,
viewModel: ReaderSettingsViewModel,
screenModel: ReaderSettingsScreenModel,
onChange: (StringResource) -> Unit,
) {
val manga by viewModel.mangaFlow.collectAsState()
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
AdaptiveSheet(onDismissRequest = onDismissRequest) {
DialogContent(
readingMode = readingMode,
onChangeReadingMode = {
viewModel.onChangeReadingMode(it)
screenModel.onChangeReadingMode(it)
onChange(it.stringRes)
onDismissRequest()
},
@@ -65,7 +65,6 @@ fun ReaderAppBars(
currentPage: Int,
totalPages: Int,
onPageIndexChange: (Int) -> Unit,
onPageIndexChangeFinished: () -> Unit,
readingMode: ReadingMode,
onClickReadingMode: () -> Unit,
@@ -129,7 +128,6 @@ fun ReaderAppBars(
currentPage = currentPage,
totalPages = totalPages,
onPageIndexChange = onPageIndexChange,
onPageIndexChangeFinished = onPageIndexChangeFinished,
)
}
}
@@ -157,7 +155,6 @@ fun ReaderAppBars(
currentPage = currentPage,
totalPages = totalPages,
onPageIndexChange = onPageIndexChange,
onPageIndexChangeFinished = onPageIndexChangeFinished,
)
}
ReaderBottomBar(
@@ -70,7 +70,6 @@ fun ChapterNavigator(
currentPage: Int,
totalPages: Int,
onPageIndexChange: (Int) -> Unit,
onPageIndexChangeFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
val haptic = LocalHapticFeedback.current
@@ -84,7 +83,6 @@ fun ChapterNavigator(
}
state.value = currentPage.toFloat()
state.onValueChange = { onPageIndexChange(it.roundToInt() - 1) }
state.onValueChangeFinished = onPageIndexChangeFinished
val interactionSource = remember { MutableInteractionSource() }
val sliderDragged by interactionSource.collectIsDraggedAsState()
@@ -312,7 +310,6 @@ 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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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(viewModel: ReaderSettingsViewModel) {
val customBrightness by viewModel.preferences.customBrightness.collectAsState()
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
val customBrightness by screenModel.preferences.customBrightness.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_custom_brightness),
pref = viewModel.preferences.customBrightness,
pref = screenModel.preferences.customBrightness,
)
/*
@@ -35,31 +35,31 @@ internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
* 0 sets system brightness and hides the overlay.
*/
if (customBrightness) {
val customBrightnessValue by viewModel.preferences.customBrightnessValue.collectAsState()
val customBrightnessValue by screenModel.preferences.customBrightnessValue.collectAsState()
SliderItem(
value = customBrightnessValue,
valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness),
onChange = { viewModel.preferences.customBrightnessValue.set(it) },
onChange = { screenModel.preferences.customBrightnessValue.set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
val colorFilter by viewModel.preferences.colorFilter.collectAsState()
val colorFilter by screenModel.preferences.colorFilter.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_custom_color_filter),
pref = viewModel.preferences.colorFilter,
pref = screenModel.preferences.colorFilter,
)
if (colorFilter) {
val colorFilterValue by viewModel.preferences.colorFilterValue.collectAsState()
val colorFilterValue by screenModel.preferences.colorFilterValue.collectAsState()
SliderItem(
value = colorFilterValue.red,
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value),
onChange = { newRValue ->
viewModel.preferences.colorFilterValue.getAndSet {
screenModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
},
@@ -71,7 +71,7 @@ internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
steps = 0,
label = stringResource(MR.strings.color_filter_g_value),
onChange = { newGValue ->
viewModel.preferences.colorFilterValue.getAndSet {
screenModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8)
}
},
@@ -83,7 +83,7 @@ internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
steps = 0,
label = stringResource(MR.strings.color_filter_b_value),
onChange = { newBValue ->
viewModel.preferences.colorFilterValue.getAndSet {
screenModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0)
}
},
@@ -95,19 +95,19 @@ internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
steps = 0,
label = stringResource(MR.strings.color_filter_a_value),
onChange = { newAValue ->
viewModel.preferences.colorFilterValue.getAndSet {
screenModel.preferences.colorFilterValue.getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val colorFilterMode by viewModel.preferences.colorFilterMode.collectAsState()
val colorFilterMode by screenModel.preferences.colorFilterMode.collectAsState()
SettingsChipRow(MR.strings.pref_color_filter_mode) {
ColorFilterMode.mapIndexed { index, it ->
FilterChip(
selected = colorFilterMode == index,
onClick = { viewModel.preferences.colorFilterMode.set(index) },
onClick = { screenModel.preferences.colorFilterMode.set(index) },
label = { Text(stringResource(it.first)) },
)
}
@@ -116,11 +116,11 @@ internal fun ColumnScope.ColorFilterPage(viewModel: ReaderSettingsViewModel) {
CheckboxItem(
label = stringResource(MR.strings.pref_grayscale),
pref = viewModel.preferences.grayscale,
pref = screenModel.preferences.grayscale,
)
CheckboxItem(
label = stringResource(MR.strings.pref_inverted_colors),
pref = viewModel.preferences.invertedColors,
pref = screenModel.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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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(viewModel: ReaderSettingsViewModel) {
val readerTheme by viewModel.preferences.readerTheme.collectAsState()
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
val readerTheme by screenModel.preferences.readerTheme.collectAsState()
val flashPageState by viewModel.preferences.flashOnPageChange.collectAsState()
val flashPageState by screenModel.preferences.flashOnPageChange.collectAsState()
val flashMillisPref = viewModel.preferences.flashDurationMillis
val flashMillisPref = screenModel.preferences.flashDurationMillis
val flashMillis by flashMillisPref.collectAsState()
val flashIntervalPref = viewModel.preferences.flashPageInterval
val flashIntervalPref = screenModel.preferences.flashPageInterval
val flashInterval by flashIntervalPref.collectAsState()
val flashColorPref = viewModel.preferences.flashColor
val flashColorPref = screenModel.preferences.flashColor
val flashColor by flashColorPref.collectAsState()
SettingsChipRow(MR.strings.pref_reader_theme) {
themes.map { (labelRes, value) ->
FilterChip(
selected = readerTheme == value,
onClick = { viewModel.preferences.readerTheme.set(value) },
onClick = { screenModel.preferences.readerTheme.set(value) },
label = { Text(stringResource(labelRes)) },
)
}
@@ -59,10 +59,10 @@ internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
CheckboxItem(
label = stringResource(MR.strings.pref_show_page_number),
pref = viewModel.preferences.showPageNumber,
pref = screenModel.preferences.showPageNumber,
)
val verticalNavigatorModes by viewModel.preferences.verticalNavigator.collectAsState()
val verticalNavigatorModes by screenModel.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(viewModel: ReaderSettingsViewModel) {
} else {
verticalNavigatorModes + mode
}
viewModel.preferences.verticalNavigator.set(newModes)
screenModel.preferences.verticalNavigator.set(newModes)
},
label = { Text(stringResource(mode.stringRes)) },
)
@@ -82,12 +82,12 @@ internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
}
if (verticalNavigatorModes.isNotEmpty()) {
val verticalNavigatorHeightPref = viewModel.preferences.verticalNavigatorHeight
val verticalNavigatorHeightPref = screenModel.preferences.verticalNavigatorHeight
val verticalNavigatorHeight by verticalNavigatorHeightPref.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_vertical_navigator_on_left),
pref = viewModel.preferences.verticalNavigatorOnLeft,
pref = screenModel.preferences.verticalNavigatorOnLeft,
)
SliderItem(
@@ -101,40 +101,40 @@ internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
CheckboxItem(
label = stringResource(MR.strings.pref_fullscreen),
pref = viewModel.preferences.fullscreen,
pref = screenModel.preferences.fullscreen,
)
val isFullscreen by viewModel.preferences.fullscreen.collectAsState()
val isFullscreen by screenModel.preferences.fullscreen.collectAsState()
if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
CheckboxItem(
label = stringResource(MR.strings.pref_cutout_short),
pref = viewModel.preferences.drawUnderCutout,
pref = screenModel.preferences.drawUnderCutout,
)
}
CheckboxItem(
label = stringResource(MR.strings.pref_keep_screen_on),
pref = viewModel.preferences.keepScreenOn,
pref = screenModel.preferences.keepScreenOn,
)
CheckboxItem(
label = stringResource(MR.strings.pref_read_with_long_tap),
pref = viewModel.preferences.readWithLongTap,
pref = screenModel.preferences.readWithLongTap,
)
CheckboxItem(
label = stringResource(MR.strings.pref_always_show_chapter_transition),
pref = viewModel.preferences.alwaysShowChapterTransition,
pref = screenModel.preferences.alwaysShowChapterTransition,
)
CheckboxItem(
label = stringResource(MR.strings.pref_page_transitions),
pref = viewModel.preferences.pageTransitions,
pref = screenModel.preferences.pageTransitions,
)
CheckboxItem(
label = stringResource(MR.strings.pref_flash_page),
pref = viewModel.preferences.flashOnPageChange,
pref = screenModel.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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@@ -23,7 +23,7 @@ fun ReaderSettingsDialog(
onDismissRequest: () -> Unit,
onShowMenus: () -> Unit,
onHideMenus: () -> Unit,
viewModel: ReaderSettingsViewModel,
screenModel: ReaderSettingsScreenModel,
) {
val tabTitles = listOf(
stringResource(MR.strings.pref_category_reading_mode),
@@ -60,9 +60,9 @@ fun ReaderSettingsDialog(
.verticalScroll(rememberScrollState()),
) {
when (page) {
0 -> ReadingModePage(viewModel)
1 -> GeneralPage(viewModel)
2 -> ColorFilterPage(viewModel)
0 -> ReadingModePage(screenModel)
1 -> GeneralPage(screenModel)
2 -> ColorFilterPage(screenModel)
}
}
}
@@ -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.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
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(viewModel: ReaderSettingsViewModel) {
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
HeadingItem(MR.strings.pref_category_for_this_series)
val manga by viewModel.mangaFlow.collectAsState()
val manga by screenModel.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 = { viewModel.onChangeReadingMode(it) },
onClick = { screenModel.onChangeReadingMode(it) },
label = { Text(stringResource(it.stringRes)) },
)
}
@@ -45,50 +45,50 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
ReaderOrientation.entries.map {
FilterChip(
selected = it == orientation,
onClick = { viewModel.onChangeOrientation(it) },
onClick = { screenModel.onChangeOrientation(it) },
label = { Text(stringResource(it.stringRes)) },
)
}
}
val viewer by viewModel.viewerFlow.collectAsState()
val viewer by screenModel.viewerFlow.collectAsState()
if (viewer is WebtoonViewer) {
WebtoonViewerSettings(viewModel)
WebtoonViewerSettings(screenModel)
} else {
PagerViewerSettings(viewModel)
PagerViewerSettings(screenModel)
}
}
@Composable
private fun ColumnScope.PagerViewerSettings(viewModel: ReaderSettingsViewModel) {
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
HeadingItem(MR.strings.pager_viewer)
val navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
val navigationModePager by screenModel.preferences.navigationModePager.collectAsState()
val pagerNavInverted by screenModel.preferences.pagerNavInverted.collectAsState()
TapZonesItems(
selected = navigationModePager,
onSelect = viewModel.preferences.navigationModePager::set,
onSelect = screenModel.preferences.navigationModePager::set,
invertMode = pagerNavInverted,
onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
onSelectInvertMode = screenModel.preferences.pagerNavInverted::set,
)
val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
val imageScaleType by screenModel.preferences.imageScaleType.collectAsState()
SettingsChipRow(MR.strings.pref_image_scale_type) {
ReaderPreferences.ImageScaleType.mapIndexed { index, it ->
FilterChip(
selected = imageScaleType == index + 1,
onClick = { viewModel.preferences.imageScaleType.set(index + 1) },
onClick = { screenModel.preferences.imageScaleType.set(index + 1) },
label = { Text(stringResource(it)) },
)
}
}
val zoomStart by viewModel.preferences.zoomStart.collectAsState()
val zoomStart by screenModel.preferences.zoomStart.collectAsState()
SettingsChipRow(MR.strings.pref_zoom_start) {
ReaderPreferences.ZoomStart.mapIndexed { index, it ->
FilterChip(
selected = zoomStart == index + 1,
onClick = { viewModel.preferences.zoomStart.set(index + 1) },
onClick = { screenModel.preferences.zoomStart.set(index + 1) },
label = { Text(stringResource(it)) },
)
}
@@ -96,111 +96,111 @@ private fun ColumnScope.PagerViewerSettings(viewModel: ReaderSettingsViewModel)
CheckboxItem(
label = stringResource(MR.strings.pref_crop_borders),
pref = viewModel.preferences.cropBorders,
pref = screenModel.preferences.cropBorders,
)
CheckboxItem(
label = stringResource(MR.strings.pref_landscape_zoom),
pref = viewModel.preferences.landscapeZoom,
pref = screenModel.preferences.landscapeZoom,
)
CheckboxItem(
label = stringResource(MR.strings.pref_navigate_pan),
pref = viewModel.preferences.navigateToPan,
pref = screenModel.preferences.navigateToPan,
)
val dualPageSplitPaged by viewModel.preferences.dualPageSplitPaged.collectAsState()
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_split),
pref = viewModel.preferences.dualPageSplitPaged,
pref = screenModel.preferences.dualPageSplitPaged,
)
if (dualPageSplitPaged) {
CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_invert),
pref = viewModel.preferences.dualPageInvertPaged,
pref = screenModel.preferences.dualPageInvertPaged,
)
}
val dualPageRotateToFit by viewModel.preferences.dualPageRotateToFit.collectAsState()
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate),
pref = viewModel.preferences.dualPageRotateToFit,
pref = screenModel.preferences.dualPageRotateToFit,
)
if (dualPageRotateToFit) {
CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate_invert),
pref = viewModel.preferences.dualPageRotateToFitInvert,
pref = screenModel.preferences.dualPageRotateToFitInvert,
)
}
}
@Composable
private fun ColumnScope.WebtoonViewerSettings(viewModel: ReaderSettingsViewModel) {
private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) {
val numberFormat = remember { NumberFormat.getPercentInstance() }
HeadingItem(MR.strings.webtoon_viewer)
val navigationModeWebtoon by viewModel.preferences.navigationModeWebtoon.collectAsState()
val webtoonNavInverted by viewModel.preferences.webtoonNavInverted.collectAsState()
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon.collectAsState()
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted.collectAsState()
TapZonesItems(
selected = navigationModeWebtoon,
onSelect = viewModel.preferences.navigationModeWebtoon::set,
onSelect = screenModel.preferences.navigationModeWebtoon::set,
invertMode = webtoonNavInverted,
onSelectInvertMode = viewModel.preferences.webtoonNavInverted::set,
onSelectInvertMode = screenModel.preferences.webtoonNavInverted::set,
)
val webtoonSidePadding by viewModel.preferences.webtoonSidePadding.collectAsState()
val webtoonSidePadding by screenModel.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 = {
viewModel.preferences.webtoonSidePadding.set(it)
screenModel.preferences.webtoonSidePadding.set(it)
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
CheckboxItem(
label = stringResource(MR.strings.pref_crop_borders),
pref = viewModel.preferences.cropBordersWebtoon,
pref = screenModel.preferences.cropBordersWebtoon,
)
val dualPageSplitWebtoon by viewModel.preferences.dualPageSplitWebtoon.collectAsState()
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_split),
pref = viewModel.preferences.dualPageSplitWebtoon,
pref = screenModel.preferences.dualPageSplitWebtoon,
)
if (dualPageSplitWebtoon) {
CheckboxItem(
label = stringResource(MR.strings.pref_dual_page_invert),
pref = viewModel.preferences.dualPageInvertWebtoon,
pref = screenModel.preferences.dualPageInvertWebtoon,
)
}
val dualPageRotateToFitWebtoon by viewModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
val dualPageRotateToFitWebtoon by screenModel.preferences.dualPageRotateToFitWebtoon.collectAsState()
CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate),
pref = viewModel.preferences.dualPageRotateToFitWebtoon,
pref = screenModel.preferences.dualPageRotateToFitWebtoon,
)
if (dualPageRotateToFitWebtoon) {
CheckboxItem(
label = stringResource(MR.strings.pref_page_rotate_invert),
pref = viewModel.preferences.dualPageRotateToFitInvertWebtoon,
pref = screenModel.preferences.dualPageRotateToFitInvertWebtoon,
)
}
CheckboxItem(
label = stringResource(MR.strings.pref_double_tap_zoom),
pref = viewModel.preferences.webtoonDoubleTapZoomEnabled,
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled,
)
CheckboxItem(
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
pref = viewModel.preferences.webtoonDisableZoomOut,
pref = screenModel.preferences.webtoonDisableZoomOut,
)
}
@@ -22,7 +22,6 @@ 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
@@ -91,7 +90,6 @@ 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,
@@ -1,89 +0,0 @@
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.UpdatesSettingsViewModel
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
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,
viewModel: UpdatesSettingsViewModel,
screenModel: UpdatesSettingsScreenModel,
) {
TabbedDialog(
onDismissRequest = onDismissRequest,
@@ -45,48 +45,48 @@ fun UpdatesFilterDialog(
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
FilterSheet(viewModel = viewModel)
FilterSheet(screenModel = screenModel)
}
}
}
@Composable
private fun ColumnScope.FilterSheet(
viewModel: UpdatesSettingsViewModel,
screenModel: UpdatesSettingsScreenModel,
) {
val filterDownloaded by viewModel.updatesPreferences.filterDownloaded.collectAsState()
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded.collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
state = filterDownloaded,
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
)
val filterUnread by viewModel.updatesPreferences.filterUnread.collectAsState()
val filterUnread by screenModel.updatesPreferences.filterUnread.collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_unread),
state = filterUnread,
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterUnread) },
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
)
val filterStarted by viewModel.updatesPreferences.filterStarted.collectAsState()
val filterStarted by screenModel.updatesPreferences.filterStarted.collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_started),
state = filterStarted,
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterStarted) },
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
)
val filterBookmarked by viewModel.updatesPreferences.filterBookmarked.collectAsState()
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked.collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked),
state = filterBookmarked,
onClick = { viewModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
)
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
val filterExcludedScanlators by viewModel.updatesPreferences.filterExcludedScanlators.collectAsState()
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators.collectAsState()
fun toggleScanlatorFilter() = viewModel.updatesPreferences.filterExcludedScanlators.getAndSet { !it }
fun toggleScanlatorFilter() = screenModel.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.UpdatesViewModel
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel
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: UpdatesViewModel.State,
state: UpdatesScreenModel.State,
snackbarHostState: SnackbarHostState,
lastUpdated: Long,
onClickCover: (UpdatesItem) -> Unit,
@@ -37,6 +37,7 @@ 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
@@ -91,14 +92,14 @@ internal fun LazyListScope.updatesUiItems(
when (item) {
is UpdatesUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
text = relativeDateText(item.date),
)
}
is UpdatesUiModel.Item -> {
val updatesItem = item.item
UpdatesUiItem(
modifier = Modifier.animateItem(),
modifier = Modifier.animateItemFastScroll(),
update = updatesItem.update,
selected = updatesItem.selected,
readProgress = updatesItem.update.lastPageRead
@@ -0,0 +1,10 @@
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,12 +8,20 @@ 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
@@ -31,6 +39,18 @@ 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,6 +3,7 @@ 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,7 +2,9 @@ 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,30 +148,13 @@ class BackupRestorer(
mangaRestorer.sortByNew(backupMangas)
.chunked(100)
.forEach { chunk ->
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 {
database.transaction {
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}")
}
@@ -179,7 +162,6 @@ class BackupRestorer(
restoreProgress.incrementAndFetch()
}
}
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
}
}
@@ -11,6 +11,7 @@ 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
@@ -284,7 +285,7 @@ class MangaRestorer(
restoreCategories(manga, categories, backupCategories)
restoreChapters(manga, chapters)
restoreTracking(manga, tracks)
restoreHistory(manga, history)
restoreHistory(history)
restoreExcludedScanlators(manga, excludedScanlators)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
return manga
@@ -324,16 +325,16 @@ class MangaRestorer(
}
}
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
val toUpdate = backupHistory.mapNotNull { history ->
val dbHistory = database.historyQueries
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
.getHistoryByChapterUrl(history.url)
.awaitAsOneOrNull()
val item = history.getHistoryImpl()
if (dbHistory == null) {
val chapter = database.chaptersQueries
.getChapterByUrlAndMangaId(history.url, manga.id)
.getChapterByUrl(history.url)
.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, file.length()).use {
source.getImage(page).use {
it.body.source().saveTo(
// If the server supports partial downloads (HTTP 206),
// append to the existing file.
@@ -9,6 +9,7 @@ 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
@@ -71,6 +72,10 @@ 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(
@@ -173,6 +178,15 @@ 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
*
@@ -227,6 +241,9 @@ 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"
@@ -507,6 +524,45 @@ 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,6 +70,8 @@ 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,6 +30,7 @@ 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
@@ -51,7 +52,7 @@ class MangaBakaApi(
.header(
"User-Agent",
buildString {
append("Mihon/v${BuildConfig.VERSION_NAME} ")
append("${MR.strings.app_name}/v${BuildConfig.VERSION_NAME} ")
append("(${BuildConfig.APPLICATION_ID} ${BuildConfig.COMMIT_SHA}) ")
append("(Android) (https://github.com/mihonapp/mihon)")
},
@@ -1,5 +1,6 @@
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
@@ -11,7 +12,7 @@ class AppUpdateChecker {
private val getApplicationRelease: GetApplicationRelease by injectLazy()
suspend fun checkForUpdate(forceCheck: Boolean = false): GetApplicationRelease.Result {
suspend fun checkForUpdate(context: Context, 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
@@ -29,6 +30,11 @@ class AppUpdateChecker {
),
)
when (result) {
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
else -> {}
}
result
}
}
@@ -15,26 +15,30 @@ 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.awaitSuccess
import eu.kanade.tachiyomi.network.await
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 kotlinx.coroutines.CancellationException
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
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()
@@ -42,27 +46,17 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
setForegroundSafely()
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()
withIOContext {
downloadApk(title, url)
}
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,
notification,
notifier.onDownloadStarted().build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
@@ -76,7 +70,10 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
*
* @param url url location of file
*/
private suspend fun downloadApk(url: String) {
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
@@ -90,27 +87,45 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
setProgressAsync(workDataOf(PROGRESS to progress, EXTRA_DOWNLOAD_URL to url))
notifier.onProgressChange(progress)
}
}
}
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener).awaitSuccess()
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
val apkFile = updateApk(context)
response.body.source().saveTo(apkFile)
// 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)
}
}
}
companion object {
const val TAG = "AppUpdateDownload"
const val PROGRESS = "progress"
private const val TAG = "AppUpdateDownload"
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
fun updateApk(context: Context): File = File(context.externalCacheDir, "update.apk")
fun start(context: Context, url: String) {
fun start(context: Context, url: String, title: String? = null) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
@@ -118,7 +133,12 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(workDataOf(EXTRA_DOWNLOAD_URL to url))
.setInputData(
workDataOf(
EXTRA_DOWNLOAD_URL to url,
EXTRA_DOWNLOAD_TITLE to title,
),
)
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
@@ -0,0 +1,168 @@
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,7 +1,6 @@
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
@@ -25,7 +24,9 @@ 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
@@ -54,7 +55,6 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingleton<Context>(app)
addSingletonFactory<SqlDriver> {
synchronized(lock) {
@@ -240,8 +240,7 @@ class ExtensionManager(
*/
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
val isUpdateForPrivatelyInstalled = !extension.isShared
return installer.downloadAndInstall(availableExt.apkUrl, availableExt, isUpdateForPrivatelyInstalled)
return installExtension(availableExt)
}
fun cancelInstallUpdateExtension(extension: Extension) {
@@ -1,28 +1,53 @@
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) {
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
}
updateExtensionStores()
val extensions = findExtensions()
val extensions = if (fromAvailableExtensionList) {
extensionManager.availableExtensionsFlow.value
} else {
findExtensions().also { lastExtCheck.set(Instant.now().toEpochMilli()) }
}
val installedExtensions = ExtensionLoader.loadExtensions(context)
.filterIsInstance<LoadResult.Success>()
@@ -43,5 +68,7 @@ internal class ExtensionApi {
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
}
return extensionsWithUpdate
}
}
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
@@ -40,7 +39,6 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
.processNameSuffix("shizuku_service")
.debuggable(BuildConfig.DEBUG)
.daemon(false)
.version(2)
}
private val connection = object : ServiceConnection {
@@ -55,13 +53,6 @@ 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)
@@ -119,7 +110,7 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
super.processEntry(entry)
try {
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
shellInterface?.install(it, statusIntent.intentSender)
shellInterface?.install(it)
}
service.contentResolver.delete(entry.uri, null, null)
} catch (e: Exception) {
@@ -49,13 +49,8 @@ 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,
isUpdateForPrivatelyInstalled: Boolean = false,
): Flow<InstallStep> {
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
val downloadId = extension.pkgName.hashCode().toLong()
cancelInstall(extension.pkgName)
@@ -79,7 +74,7 @@ internal class ExtensionInstaller(
}
step.value = InstallStep.Installing
installApk(downloadId, tmpFile, isUpdateForPrivatelyInstalled)
installApk(downloadId, tmpFile)
} catch (e: Exception) {
if (e is InterruptedException) {
// Canceled
@@ -104,14 +99,8 @@ 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, isUpdateForPrivatelyInstalled: Boolean = false) {
if (isUpdateForPrivatelyInstalled) {
installApkPrivately(downloadId, tempFile)
return
}
private fun installApk(downloadId: Long, tempFile: File) {
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, ExtensionInstallActivity::class.java)
@@ -121,11 +110,20 @@ internal class ExtensionInstaller(
context.startActivity(intent)
}
BasePreferences.ExtensionInstaller.PRIVATE -> {
installApkPrivately(downloadId, tempFile)
}
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()
}
else -> {
val intent = ExtensionInstallService.getIntent(
context,
@@ -138,21 +136,6 @@ 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,7 +31,6 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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.ExtensionsViewModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
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 extensionsViewModel = viewModel<ExtensionsViewModel>()
val extensionsState by extensionsViewModel.state.collectAsState()
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() }
val extensionsState by extensionsScreenModel.state.collectAsState()
val tabs = listOf(
sourcesTab(),
extensionsTab(extensionsViewModel),
extensionsTab(extensionsScreenModel),
migrateSourceTab(),
)
@@ -74,7 +74,7 @@ data object BrowseTab : Tab {
tabs = tabs,
state = state,
searchQuery = extensionsState.searchQuery,
onChangeSearchQuery = extensionsViewModel::search,
onChangeSearchQuery = extensionsScreenModel::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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<ExtensionFilterViewModel>()
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { ExtensionFilterScreenModel() }
val state by screenModel.state.collectAsState()
if (state is ExtensionFilterState.Loading) {
LoadingScreen()
@@ -34,11 +34,11 @@ class ExtensionFilterScreen : Screen() {
ExtensionFilterScreen(
navigateUp = navigator::pop,
state = successState,
onClickToggle = viewModel::toggle,
onClickToggle = screenModel::toggle,
)
LaunchedEffect(Unit) {
viewModel.events.collectLatest {
screenModel.events.collectLatest {
when (it) {
ExtensionFilterEvent.FailedFetchingLanguages -> {
context.stringResource(MR.strings.internal_error)
@@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.service.SourcePreferences
@@ -14,22 +15,21 @@ 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 ExtensionFilterViewModel(
class ExtensionFilterScreenModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateViewModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
) : StateScreenModel<ExtensionFilterState>(ExtensionFilterState.Loading) {
private val _events: Channel<ExtensionFilterEvent> = Channel()
val events: Flow<ExtensionFilterEvent> = _events.receiveAsFlow()
init {
viewModelScope.launch {
screenModelScope.launch {
combine(
getExtensionLanguages.subscribe(),
preferences.enabledLanguages.changes(),
@@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.extension.interactor.GetExtensionsByType
@@ -27,19 +28,18 @@ 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 ExtensionsViewModel(
class ExtensionsScreenModel(
preferences: SourcePreferences = Injekt.get(),
basePreferences: BasePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensions: GetExtensionsByType = Injekt.get(),
) : StateViewModel<ExtensionsViewModel.State>(State()) {
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
private val currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
@@ -51,7 +51,7 @@ class ExtensionsViewModel(
}
}
viewModelScope.launchIO {
screenModelScope.launchIO {
combine(
state.map { it.searchQuery }
.distinctUntilChanged()
@@ -95,15 +95,15 @@ class ExtensionsViewModel(
}
}
viewModelScope.launchIO { findAvailableExtensions() }
screenModelScope.launchIO { findAvailableExtensions() }
preferences.extensionUpdatesCount.changes()
.onEach { mutableState.update { state -> state.copy(updates = it) } }
.launchIn(viewModelScope)
.launchIn(screenModelScope)
basePreferences.extensionInstaller.changes()
.onEach { mutableState.update { state -> state.copy(installer = it) } }
.launchIn(viewModelScope)
.launchIn(screenModelScope)
}
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
@@ -143,7 +143,7 @@ class ExtensionsViewModel(
}
fun updateAllExtensions() {
viewModelScope.launchIO {
screenModelScope.launchIO {
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<Extension.Installed>()
@@ -153,13 +153,13 @@ class ExtensionsViewModel(
}
fun installExtension(extension: Extension.Available) {
viewModelScope.launchIO {
screenModelScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
}
}
fun updateExtension(extension: Extension.Installed) {
viewModelScope.launchIO {
screenModelScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
}
@@ -189,7 +189,7 @@ class ExtensionsViewModel(
}
fun findAvailableExtensions() {
viewModelScope.launchIO {
screenModelScope.launchIO {
mutableState.update { it.copy(isRefreshing = true) }
extensionManager.findAvailableExtensions()
@@ -202,7 +202,7 @@ class ExtensionsViewModel(
}
fun trustExtension(extension: Extension.Untrusted) {
viewModelScope.launch {
screenModelScope.launch {
extensionManager.trust(extension)
}
}
@@ -26,12 +26,12 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun extensionsTab(
extensionsViewModel: ExtensionsViewModel,
extensionsScreenModel: ExtensionsScreenModel,
): TabContent {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val state by extensionsViewModel.state.collectAsState()
val state by extensionsScreenModel.state.collectAsState()
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
return TabContent(
@@ -50,7 +50,7 @@ fun extensionsTab(
),
content = { contentPadding, _ ->
BackHandler(enabled = state.searchQuery != null) {
extensionsViewModel.search(null)
extensionsScreenModel.search(null)
}
ExtensionScreen(
@@ -59,18 +59,18 @@ fun extensionsTab(
searchQuery = state.searchQuery,
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> extensionsViewModel.installExtension(extension)
is Extension.Available -> extensionsScreenModel.installExtension(extension)
else -> {
if (context.isPackageInstalled(extension.pkgName)) {
extensionsViewModel.uninstallExtension(extension)
extensionsScreenModel.uninstallExtension(extension)
} else {
privateExtensionToUninstall = extension
}
}
}
},
onClickItemCancel = extensionsViewModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsViewModel::updateAllExtensions,
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
onOpenWebView = { extension ->
extension.sources.getOrNull(0)?.let {
navigator.push(
@@ -82,19 +82,19 @@ fun extensionsTab(
)
}
},
onInstallExtension = extensionsViewModel::installExtension,
onInstallExtension = extensionsScreenModel::installExtension,
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
onTrustExtension = { extensionsViewModel.trustExtension(it) },
onUninstallExtension = { extensionsViewModel.uninstallExtension(it) },
onUpdateExtension = extensionsViewModel::updateExtension,
onRefresh = extensionsViewModel::findAvailableExtensions,
onTrustExtension = { extensionsScreenModel.trustExtension(it) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions,
)
privateExtensionToUninstall?.let { extension ->
ExtensionUninstallConfirmation(
extensionName = extension.name,
onClickConfirm = {
extensionsViewModel.uninstallExtension(extension)
extensionsScreenModel.uninstallExtension(extension)
},
onDismissRequest = {
privateExtensionToUninstall = null
@@ -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.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionDetailsScreen
@@ -19,13 +19,9 @@ data class ExtensionDetailsScreen(
@Composable
override fun Content() {
val viewModel = viewModel<ExtensionDetailsViewModel>(
factory = ExtensionDetailsViewModel.Factory,
extras = CreationExtras {
set(ExtensionDetailsViewModel.PKG_NAME_KEY, pkgName)
},
)
val state by viewModel.state.collectAsState()
val context = LocalContext.current
val screenModel = rememberScreenModel { ExtensionDetailsScreenModel(pkgName = pkgName, context = context) }
val state by screenModel.state.collectAsState()
if (state.isLoading) {
LoadingScreen()
@@ -38,16 +34,16 @@ data class ExtensionDetailsScreen(
navigateUp = navigator::pop,
state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickEnableAll = { viewModel.toggleSources(true) },
onClickDisableAll = { viewModel.toggleSources(false) },
onClickClearCookies = viewModel::clearCookies,
onClickUninstall = viewModel::uninstallExtension,
onClickSource = viewModel::toggleSource,
onClickIncognito = viewModel::toggleIncognito,
onClickEnableAll = { screenModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies,
onClickUninstall = screenModel::uninstallExtension,
onClickSource = screenModel::toggleSource,
onClickIncognito = screenModel::toggleIncognito,
)
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
screenModel.events.collectLatest { event ->
if (event is ExtensionDetailsEvent.Uninstalled) {
navigator.pop()
}
@@ -1,12 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleIncognito
@@ -27,13 +24,12 @@ 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 ExtensionDetailsViewModel(
class ExtensionDetailsScreenModel(
pkgName: String,
context: Context,
private val network: NetworkHelper = Injekt.get(),
@@ -42,26 +38,13 @@ class ExtensionDetailsViewModel(
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleIncognito: ToggleIncognito = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : 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>(),
)
}
}
}
) : StateScreenModel<ExtensionDetailsScreenModel.State>(State()) {
private val _events: Channel<ExtensionDetailsEvent> = Channel()
val events: Flow<ExtensionDetailsEvent> = _events.receiveAsFlow()
init {
viewModelScope.launch {
screenModelScope.launch {
launch {
extensionManager.installedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
@@ -19,8 +19,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
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
@@ -48,14 +47,9 @@ data class MigrateMangaScreen(
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MigrateMangaViewModel>(
factory = MigrateMangaViewModel.Factory,
extras = CreationExtras {
set(MigrateMangaViewModel.SOURCE_ID_KEY, sourceId)
},
)
val screenModel = rememberScreenModel { MigrateMangaScreenModel(sourceId) }
val state by viewModel.state.collectAsState()
val state by screenModel.state.collectAsState()
if (state.isLoading) {
LoadingScreen()
@@ -63,7 +57,7 @@ data class MigrateMangaScreen(
}
BackHandler(enabled = state.selectionMode) {
viewModel.clearSelection()
screenModel.clearSelection()
}
val lazyListState = rememberLazyListState()
@@ -74,7 +68,7 @@ data class MigrateMangaScreen(
title = state.source!!.name,
navigateUp = {
if (state.selectionMode) {
viewModel.clearSelection()
screenModel.clearSelection()
} else {
navigator.pop()
}
@@ -90,7 +84,7 @@ data class MigrateMangaScreen(
},
onClick = {
val selection = state.selection
viewModel.clearSelection()
screenModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
expanded = lazyListState.shouldExpandFAB(),
@@ -113,13 +107,13 @@ data class MigrateMangaScreen(
lazyListState = lazyListState,
contentPadding = contentPadding,
state = state,
onClickItem = viewModel::toggleSelection,
onClickItem = screenModel::toggleSelection,
onClickCover = { navigator.push(MangaScreen(it.id)) },
)
}
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
screenModel.events.collectLatest { event ->
when (event) {
MigrationMangaEvent.FailedFetchingFavorites -> {
context.toast(MR.strings.internal_error)
@@ -133,7 +127,7 @@ data class MigrateMangaScreen(
private fun MigrateMangaContent(
lazyListState: LazyListState,
contentPadding: PaddingValues,
state: MigrateMangaViewModel.State,
state: MigrateMangaScreenModel.State,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
@@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
@@ -16,7 +14,6 @@ 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
@@ -24,29 +21,17 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateMangaViewModel(
class MigrateMangaScreenModel(
private val sourceId: Long,
private val sourceManager: SourceManager = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(),
) : StateViewModel<MigrateMangaViewModel.State>(State()) {
companion object {
val SOURCE_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateMangaViewModel(
sourceId = get(SOURCE_ID_KEY)!!,
)
}
}
}
) : StateScreenModel<MigrateMangaScreenModel.State>(State()) {
private val _events: Channel<MigrationMangaEvent> = Channel()
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
init {
viewModelScope.launch {
screenModelScope.launch {
mutableState.update { state ->
state.copy(source = sourceManager.getOrStub(sourceId))
}
@@ -3,13 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.feature.migration.list.MigrationListScreen
@@ -20,23 +19,18 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<MigrateSearchViewModel>(
factory = MigrateSearchViewModel.Factory,
extras = CreationExtras {
set(MigrateSearchViewModel.MANGA_ID_KEY, mangaId)
},
)
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val state by screenModel.state.collectAsState()
MigrateSearchScreen(
state = state,
fromSourceId = state.from?.source,
navigateUp = navigator::pop,
onChangeSearchQuery = viewModel::updateSearchQuery,
onSearch = { viewModel.search() },
getManga = { viewModel.getManga(it) },
onChangeSearchFilter = viewModel::setSourceFilter,
onToggleResults = viewModel::toggleFilterResults,
onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = { screenModel.search() },
getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
onClickItem = {
val migrateListScreen = navigator.items
@@ -44,7 +38,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
.lastOrNull()
if (migrateListScreen == null) {
viewModel.setMigrateDialog(mangaId, it)
screenModel.setMigrateDialog(mangaId, it)
} else {
migrateListScreen.addMatchOverride(current = mangaId, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen }
@@ -54,13 +48,13 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
)
when (val dialog = state.dialog) {
is SearchViewModel.Dialog.Migrate -> {
is SearchScreenModel.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 = { viewModel.clearDialog() },
onDismissRequest = { screenModel.clearDialog() },
onComplete = {
if (navigator.lastItem is MangaScreen) {
val lastItem = navigator.lastItem
@@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.core.model.screenModelScope
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.SearchViewModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
@@ -15,24 +12,12 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateSearchViewModel(
class MigrateSearchScreenModel(
val mangaId: Long,
getManga: GetManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : SearchViewModel() {
companion object {
val MANGA_ID_KEY = CreationExtras.Key<Long>()
val Factory = viewModelFactory {
initializer {
MigrateSearchViewModel(
mangaId = get(MANGA_ID_KEY)!!,
)
}
}
}
) : SearchScreenModel() {
private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
@@ -44,7 +29,7 @@ class MigrateSearchViewModel(
}
init {
viewModelScope.launch {
screenModelScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {
it.copy(
@@ -17,8 +17,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded
@@ -26,7 +25,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.BrowseSourceViewModel
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
@@ -60,14 +59,8 @@ data class MigrateSourceSearchScreen(
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
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 screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, query) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
@@ -75,9 +68,9 @@ data class MigrateSourceSearchScreen(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = state.toolbarQuery ?: "",
onChangeSearchQuery = viewModel::setToolbarQuery,
onChangeSearchQuery = screenModel::setToolbarQuery,
onClickCloseSearch = navigator::pop,
onSearch = viewModel::search,
onSearch = screenModel::search,
scrollBehavior = scrollBehavior,
)
},
@@ -85,7 +78,7 @@ data class MigrateSourceSearchScreen(
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
onClick = viewModel::openFilterSheet,
onClick = screenModel::openFilterSheet,
modifier = Modifier.animateFloatingActionButton(
visible = state.filters.isNotEmpty(),
alignment = Alignment.BottomEnd,
@@ -100,21 +93,21 @@ data class MigrateSourceSearchScreen(
.lastOrNull()
if (migrateListScreen == null) {
viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(target = it, current = currentManga))
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(target = it, current = currentManga))
} else {
migrateListScreen.addMatchOverride(current = currentManga.id, target = it.id)
navigator.popUntil { screen -> screen is MigrationListScreen }
}
}
BrowseSourceContent(
source = viewModel.source,
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = viewModel.displayMode,
source = screenModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = {
val source = viewModel.source as? HttpSource ?: return@BrowseSourceContent
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
navigator.push(
WebViewScreen(
url = source.getHomeUrl(),
@@ -130,18 +123,18 @@ data class MigrateSourceSearchScreen(
)
}
val onDismissRequest = { viewModel.setDialog(null) }
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceViewModel.Dialog.Filter -> {
is BrowseSourceScreenModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
onReset = viewModel::resetFilters,
onFilter = { viewModel.search(filters = state.filters) },
onUpdate = viewModel::setFilters,
onReset = screenModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters,
)
}
is BrowseSourceViewModel.Dialog.Migrate -> {
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = currentManga,
target = dialog.target,
@@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.service.SourcePreferences
@@ -13,24 +14,23 @@ 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 MigrateSourceViewModel(
class MigrateSourceScreenModel(
preferences: SourcePreferences = Injekt.get(),
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : StateViewModel<MigrateSourceViewModel.State>(State()) {
) : StateScreenModel<MigrateSourceScreenModel.State>(State()) {
private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow()
init {
viewModelScope.launchIO {
screenModelScope.launchIO {
getSourcesWithFavoriteCount.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
@@ -48,11 +48,11 @@ class MigrateSourceViewModel(
preferences.migrationSortingDirection.changes()
.onEach { mutableState.update { state -> state.copy(sortingDirection = it) } }
.launchIn(viewModelScope)
.launchIn(screenModelScope)
preferences.migrationSortingMode.changes()
.onEach { mutableState.update { state -> state.copy(sortingMode = it) } }
.launchIn(viewModelScope)
.launchIn(screenModelScope)
}
fun toggleSortingMode() {
@@ -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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<MigrateSourceViewModel>()
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
val state by screenModel.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 = viewModel::toggleSortingDirection,
onToggleSortingMode = viewModel::toggleSortingMode,
onToggleSortingDirection = screenModel::toggleSortingDirection,
onToggleSortingMode = screenModel::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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<SourcesFilterViewModel>()
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { SourcesFilterScreenModel() }
val state by screenModel.state.collectAsState()
if (state is SourcesFilterViewModel.State.Loading) {
if (state is SourcesFilterScreenModel.State.Loading) {
LoadingScreen()
return
}
if (state is SourcesFilterViewModel.State.Error) {
if (state is SourcesFilterScreenModel.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 SourcesFilterViewModel.State.Success
val successState = state as SourcesFilterScreenModel.State.Success
SourcesFilterScreen(
navigateUp = navigator::pop,
state = successState,
onClickLanguage = viewModel::toggleLanguage,
onClickSource = viewModel::toggleSource,
onClickLanguage = screenModel::toggleLanguage,
onClickSource = screenModel::toggleSource,
)
}
}
@@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
@@ -11,21 +12,20 @@ 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 SourcesFilterViewModel(
class SourcesFilterScreenModel(
private val preferences: SourcePreferences = Injekt.get(),
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
) : StateViewModel<SourcesFilterViewModel.State>(State.Loading) {
) : StateScreenModel<SourcesFilterScreenModel.State>(State.Loading) {
init {
viewModelScope.launch {
screenModelScope.launch {
combine(
getLanguagesWithSources.subscribe(),
preferences.enabledLanguages.changes(),
@@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@@ -12,7 +13,6 @@ 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 SourcesViewModel(
class SourcesScreenModel(
private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : StateViewModel<SourcesViewModel.State>(State()) {
) : StateScreenModel<SourcesScreenModel.State>(State()) {
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
init {
viewModelScope.launchIO {
screenModelScope.launchIO {
getEnabledSources.subscribe()
.catch {
logcat(LogPriority.ERROR, it)
@@ -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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<SourcesViewModel>()
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { SourcesScreenModel() }
val state by screenModel.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 = viewModel::togglePin,
onLongClickItem = viewModel::showSourceDialog,
onClickPin = screenModel::togglePin,
onLongClickItem = screenModel::showSourceDialog,
)
state.dialog?.let { dialog ->
@@ -58,22 +58,22 @@ fun Screen.sourcesTab(): TabContent {
SourceOptionsDialog(
source = source,
onClickPin = {
viewModel.togglePin(source)
viewModel.closeDialog()
screenModel.togglePin(source)
screenModel.closeDialog()
},
onClickDisable = {
viewModel.toggleSource(source)
viewModel.closeDialog()
screenModel.toggleSource(source)
screenModel.closeDialog()
},
onDismiss = viewModel::closeDialog,
onDismiss = screenModel::closeDialog,
)
}
val internalErrString = stringResource(MR.strings.internal_error)
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
screenModel.events.collectLatest { event ->
when (event) {
SourcesViewModel.Event.FailedFetchingSources -> {
SourcesScreenModel.Event.FailedFetchingSources -> {
launch { snackbarHostState.showSnackbar(internalErrString) }
}
}
@@ -32,8 +32,7 @@ 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 androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded
@@ -47,7 +46,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.BrowseSourceViewModel.Listing
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
@@ -82,26 +81,20 @@ data class BrowseSourceScreen(
return
}
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 screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId, listingQuery) }
val state by screenModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val navigateUp: () -> Unit = {
when {
!state.isUserQuery && state.toolbarQuery != null -> viewModel.setToolbarQuery(null)
!state.isUserQuery && state.toolbarQuery != null -> screenModel.setToolbarQuery(null)
else -> navigator.pop()
}
}
if (viewModel.source is StubSource) {
if (screenModel.source is StubSource) {
MissingSourceScreen(
source = viewModel.source,
source = screenModel.source,
navigateUp = navigateUp,
)
return
@@ -114,7 +107,7 @@ data class BrowseSourceScreen(
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
val onWebViewClick = f@{
val source = viewModel.source as? HttpSource ?: return@f
val source = screenModel.source as? HttpSource ?: return@f
navigator.push(
WebViewScreen(
url = source.getHomeUrl(),
@@ -124,8 +117,8 @@ data class BrowseSourceScreen(
)
}
LaunchedEffect(viewModel.source) {
assistUrl = (viewModel.source as? HttpSource)?.getHomeUrl()
LaunchedEffect(screenModel.source) {
assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl()
}
Scaffold(
@@ -137,15 +130,15 @@ data class BrowseSourceScreen(
) {
BrowseSourceToolbar(
searchQuery = state.toolbarQuery,
onSearchQueryChange = viewModel::setToolbarQuery,
source = viewModel.source,
displayMode = viewModel.displayMode,
onDisplayModeChange = { viewModel.displayMode = it },
onSearchQueryChange = screenModel::setToolbarQuery,
source = screenModel.source,
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
onSearch = viewModel::search,
onSearch = screenModel::search,
)
Row(
@@ -157,8 +150,8 @@ data class BrowseSourceScreen(
FilterChip(
selected = state.listing == Listing.Popular,
onClick = {
viewModel.resetFilters()
viewModel.setListing(Listing.Popular)
screenModel.resetFilters()
screenModel.setListing(Listing.Popular)
},
leadingIcon = {
Icon(
@@ -172,12 +165,12 @@ data class BrowseSourceScreen(
Text(text = stringResource(MR.strings.popular))
},
)
if (viewModel.source.supportsLatest) {
if (screenModel.source.supportsLatest) {
FilterChip(
selected = state.listing == Listing.Latest,
onClick = {
viewModel.resetFilters()
viewModel.setListing(Listing.Latest)
screenModel.resetFilters()
screenModel.setListing(Listing.Latest)
},
leadingIcon = {
Icon(
@@ -195,7 +188,7 @@ data class BrowseSourceScreen(
if (state.filters.isNotEmpty()) {
FilterChip(
selected = state.listing is Listing.Search,
onClick = viewModel::openFilterSheet,
onClick = screenModel::openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
@@ -217,10 +210,10 @@ data class BrowseSourceScreen(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
BrowseSourceContent(
source = viewModel.source,
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = viewModel.displayMode,
source = screenModel.source,
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
@@ -229,13 +222,13 @@ data class BrowseSourceScreen(
onMangaClick = { navigator.push((MangaScreen(it.id, true))) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicates = viewModel.getDuplicateLibraryManga(manga)
val duplicates = screenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> viewModel.setDialog(BrowseSourceViewModel.Dialog.RemoveManga(manga))
duplicates.isNotEmpty() -> viewModel.setDialog(
BrowseSourceViewModel.Dialog.AddDuplicateManga(manga, duplicates),
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicates.isNotEmpty() -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
)
else -> viewModel.addFavorite(manga)
else -> screenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
@@ -243,28 +236,28 @@ data class BrowseSourceScreen(
)
}
val onDismissRequest = { viewModel.setDialog(null) }
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceViewModel.Dialog.Filter -> {
is BrowseSourceScreenModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
onReset = viewModel::resetFilters,
onFilter = { viewModel.search(filters = state.filters) },
onUpdate = viewModel::setFilters,
onReset = screenModel::resetFilters,
onFilter = { screenModel.search(filters = state.filters) },
onUpdate = screenModel::setFilters,
)
}
is BrowseSourceViewModel.Dialog.AddDuplicateManga -> {
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest,
onConfirm = { viewModel.addFavorite(dialog.manga) },
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { viewModel.setDialog(BrowseSourceViewModel.Dialog.Migrate(dialog.manga, it)) },
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) },
)
}
is BrowseSourceViewModel.Dialog.Migrate -> {
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateMangaDialog(
current = dialog.current,
target = dialog.target,
@@ -273,23 +266,23 @@ data class BrowseSourceScreen(
onDismissRequest = onDismissRequest,
)
}
is BrowseSourceViewModel.Dialog.RemoveManga -> {
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
viewModel.changeMangaFavorite(dialog.manga)
screenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is BrowseSourceViewModel.Dialog.ChangeMangaCategory -> {
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
viewModel.changeMangaFavorite(dialog.manga)
viewModel.moveMangaToCategories(dialog.manga, include)
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
@@ -300,8 +293,8 @@ data class BrowseSourceScreen(
queryEvent.receiveAsFlow()
.collectLatest {
when (it) {
is SearchType.Genre -> viewModel.searchGenre(it.txt)
is SearchType.Text -> viewModel.search(it.txt)
is SearchType.Genre -> screenModel.searchGenre(it.txt)
is SearchType.Text -> screenModel.search(it.txt)
}
}
}
@@ -6,20 +6,19 @@ 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
@@ -31,7 +30,6 @@ 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
@@ -52,7 +50,7 @@ import uy.kohesive.injekt.api.get
import java.time.Instant
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
class BrowseSourceViewModel(
class BrowseSourceScreenModel(
private val sourceId: Long,
listingQuery: String?,
sourceManager: SourceManager = Injekt.get(),
@@ -68,23 +66,9 @@ class BrowseSourceViewModel(
private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
getIncognitoState: GetIncognitoState = Injekt.get(),
) : StateViewModel<BrowseSourceViewModel.State>(State(Listing.valueOf(listingQuery))) {
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
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)
var displayMode by sourcePreferences.sourceDisplayMode.asState(screenModelScope)
val source = sourceManager.getOrStub(sourceId)
@@ -123,13 +107,13 @@ class BrowseSourceViewModel(
pagingData.map { manga ->
getManga.subscribe(manga.url, manga.source)
.map { it ?: manga }
.stateIn(viewModelScope)
.stateIn(ioCoroutineScope)
}
.filter { !hideInLibraryItems || !it.value.favorite }
}
.cachedIn(viewModelScope)
.cachedIn(ioCoroutineScope)
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyFlow())
.stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow())
fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
@@ -221,7 +205,7 @@ class BrowseSourceViewModel(
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
viewModelScope.launch {
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
@@ -242,7 +226,7 @@ class BrowseSourceViewModel(
}
fun addFavorite(manga: Manga) {
viewModelScope.launch {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory.get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@@ -297,7 +281,7 @@ class BrowseSourceViewModel(
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
viewModelScope.launchIO {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
@@ -7,8 +7,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.core.util.ifSourcesLoaded
@@ -32,14 +31,13 @@ class GlobalSearchScreen(
val navigator = LocalNavigator.currentOrThrow
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()
val screenModel = rememberScreenModel {
GlobalSearchScreenModel(
initialQuery = searchQuery,
initialExtensionFilter = extensionFilter,
)
}
val state by screenModel.state.collectAsState()
var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
}
@@ -66,11 +64,11 @@ class GlobalSearchScreen(
GlobalSearchScreen(
state = state,
navigateUp = navigator::pop,
onChangeSearchQuery = viewModel::updateSearchQuery,
onSearch = { viewModel.search() },
getManga = { viewModel.getManga(it) },
onChangeSearchFilter = viewModel::setSourceFilter,
onToggleResults = viewModel::toggleFilterResults,
onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = { screenModel.search() },
getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onClickSource = {
navigator.push(BrowseSourceScreen(it.id, state.searchQuery))
},
@@ -0,0 +1,25 @@
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 }
}
}
@@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import eu.kanade.tachiyomi.source.Source
class GlobalSearchViewModel(
initialQuery: String,
initialExtensionFilter: String?,
) : SearchViewModel(State(searchQuery = initialQuery)) {
companion object {
val INITIAL_QUERY_KEY = CreationExtras.Key<String>()
val INITIAL_EXTENSION_FILTER_KEY = CreationExtras.Key<String?>()
val Factory = viewModelFactory {
initializer {
GlobalSearchViewModel(
initialQuery = get(INITIAL_QUERY_KEY)!!,
initialExtensionFilter = get(INITIAL_EXTENSION_FILTER_KEY),
)
}
}
}
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
if (extensionFilter != null) {
// we're going to use custom extension filter instead
setSourceFilter(SourceFilter.All)
}
search()
}
}
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
}
@@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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
@@ -17,7 +19,6 @@ 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
@@ -29,7 +30,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.Executors
abstract class SearchViewModel(
abstract class SearchScreenModel(
initialState: State = State(),
sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
@@ -37,7 +38,7 @@ abstract class SearchViewModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val preferences: SourcePreferences = Injekt.get(),
) : StateViewModel<SearchViewModel.State>(initialState) {
) : StateScreenModel<SearchScreenModel.State>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
private var searchJob: Job? = null
@@ -60,7 +61,7 @@ abstract class SearchViewModel(
}
init {
viewModelScope.launch {
screenModelScope.launch {
preferences.globalSearchFilterState.changes().collectLatest { state ->
mutableState.update { it.copy(onlyShowHasResults = state) }
}
@@ -146,7 +147,7 @@ abstract class SearchViewModel(
)
}
searchJob = viewModelScope.launchIO {
searchJob = ioCoroutineScope.launch {
sources.map { source ->
async {
if (state.value.items[source] !is SearchItemResult.Loading) {
@@ -191,7 +192,7 @@ abstract class SearchViewModel(
}
fun setMigrateDialog(currentId: Long, target: Manga) {
viewModelScope.launchIO {
screenModelScope.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 androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.model.rememberScreenModel
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 viewModel = viewModel<CategoryViewModel>()
val screenModel = rememberScreenModel { CategoryScreenModel() }
val state by viewModel.state.collectAsState()
val state by screenModel.state.collectAsState()
if (state is CategoryScreenState.Loading) {
LoadingScreen()
@@ -37,10 +37,10 @@ class CategoryScreen : Screen() {
CategoryScreen(
state = successState,
onClickCreate = { viewModel.showDialog(CategoryDialog.Create) },
onClickRename = { viewModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { viewModel.showDialog(CategoryDialog.Delete(it)) },
onChangeOrder = viewModel::changeOrder,
onClickCreate = { screenModel.showDialog(CategoryDialog.Create) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
onChangeOrder = screenModel::changeOrder,
navigateUp = navigator::pop,
)
@@ -48,30 +48,30 @@ class CategoryScreen : Screen() {
null -> {}
CategoryDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = viewModel::dismissDialog,
onCreate = viewModel::createCategory,
onDismissRequest = screenModel::dismissDialog,
onCreate = screenModel::createCategory,
categories = successState.categories.fastMap { it.name },
)
}
is CategoryDialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = viewModel::dismissDialog,
onRename = { viewModel.renameCategory(dialog.category, it) },
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
categories = successState.categories.fastMap { it.name },
category = dialog.category.name,
)
}
is CategoryDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = viewModel::dismissDialog,
onDelete = { viewModel.deleteCategory(dialog.category.id) },
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) },
category = dialog.category.name,
)
}
}
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
screenModel.events.collectLatest { event ->
if (event is CategoryEvent.LocalizedMessage) {
context.toast(event.stringRes)
}
@@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.category
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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 CategoryViewModel(
class CategoryScreenModel(
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(),
) : StateViewModel<CategoryScreenState>(CategoryScreenState.Loading) {
) : StateScreenModel<CategoryScreenState>(CategoryScreenState.Loading) {
private val _events: Channel<CategoryEvent> = Channel()
val events = _events.receiveAsFlow()
init {
viewModelScope.launch {
screenModelScope.launch {
getCategories.subscribe()
.collectLatest { categories ->
mutableState.update {
@@ -45,7 +45,7 @@ class CategoryViewModel(
}
fun createCategory(name: String) {
viewModelScope.launch {
screenModelScope.launch {
when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -54,7 +54,7 @@ class CategoryViewModel(
}
fun deleteCategory(categoryId: Long) {
viewModelScope.launch {
screenModelScope.launch {
when (deleteCategory.await(categoryId = categoryId)) {
is DeleteCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -63,7 +63,7 @@ class CategoryViewModel(
}
fun changeOrder(category: Category, newIndex: Int) {
viewModelScope.launch {
screenModelScope.launch {
when (reorderCategory.await(category, newIndex)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -72,7 +72,7 @@ class CategoryViewModel(
}
fun renameCategory(category: Category, name: String) {
viewModelScope.launch {
screenModelScope.launch {
when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
@@ -6,8 +6,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
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
@@ -29,13 +28,10 @@ class DeepLinkScreen(
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val viewModel = viewModel<DeepLinkViewModel>(
factory = DeepLinkViewModel.Factory,
extras = CreationExtras {
set(DeepLinkViewModel.QUERY_KEY, query)
},
)
val state by viewModel.state.collectAsState()
val screenModel = rememberScreenModel {
DeepLinkScreenModel(query = query)
}
val state by screenModel.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
AppBar(
@@ -46,14 +42,14 @@ class DeepLinkScreen(
},
) { contentPadding ->
when (state) {
is DeepLinkViewModel.State.Loading -> {
is DeepLinkScreenModel.State.Loading -> {
LoadingScreen(Modifier.padding(contentPadding))
}
is DeepLinkViewModel.State.NoResults -> {
is DeepLinkScreenModel.State.NoResults -> {
navigator.replace(GlobalSearchScreen(query))
}
is DeepLinkViewModel.State.Result -> {
val resultState = state as DeepLinkViewModel.State.Result
is DeepLinkScreenModel.State.Result -> {
val resultState = state as DeepLinkScreenModel.State.Result
if (resultState.chapterId == null) {
navigator.replace(
MangaScreen(
@@ -1,16 +1,13 @@
package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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
@@ -22,28 +19,16 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class DeepLinkViewModel(
query: String,
class DeepLinkScreenModel(
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(),
) : StateViewModel<DeepLinkViewModel.State>(State.Loading) {
companion object {
val QUERY_KEY = CreationExtras.Key<String>()
val Factory = viewModelFactory {
initializer {
DeepLinkViewModel(
query = get(QUERY_KEY)!!,
)
}
}
}
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init {
viewModelScope.launchIO {
screenModelScope.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 viewModel = viewModel<DownloadQueueViewModel>()
val downloadList by viewModel.state.collectAsState()
val screenModel = rememberScreenModel { DownloadQueueScreenModel() }
val downloadList by screenModel.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 = {
viewModel.reorderQueue(
screenModel.reorderQueue(
{ it.download.chapter.dateUpload },
true,
)
@@ -143,7 +143,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_oldest)) },
onClick = {
viewModel.reorderQueue(
screenModel.reorderQueue(
{ it.download.chapter.dateUpload },
false,
)
@@ -158,7 +158,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_asc)) },
onClick = {
viewModel.reorderQueue(
screenModel.reorderQueue(
{ it.download.chapter.chapterNumber },
false,
)
@@ -168,7 +168,7 @@ object DownloadQueueScreen : Screen() {
DropdownMenuItem(
text = { Text(text = stringResource(MR.strings.action_desc)) },
onClick = {
viewModel.reorderQueue(
screenModel.reorderQueue(
{ it.download.chapter.chapterNumber },
true,
)
@@ -188,7 +188,7 @@ object DownloadQueueScreen : Screen() {
),
AppBar.OverflowAction(
title = stringResource(MR.strings.action_cancel_all),
onClick = { viewModel.clearQueue() },
onClick = { screenModel.clearQueue() },
),
),
)
@@ -198,7 +198,7 @@ object DownloadQueueScreen : Screen() {
)
},
floatingActionButton = {
val isRunning by viewModel.isDownloaderRunning.collectAsState()
val isRunning by screenModel.isDownloaderRunning.collectAsState()
SmallExtendedFloatingActionButton(
text = {
val id = if (isRunning) {
@@ -218,9 +218,9 @@ object DownloadQueueScreen : Screen() {
},
onClick = {
if (isRunning) {
viewModel.pauseDownloads()
screenModel.pauseDownloads()
} else {
viewModel.startDownloads()
screenModel.startDownloads()
}
},
expanded = fabExpanded,
@@ -250,27 +250,27 @@ object DownloadQueueScreen : Screen() {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { 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)
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)
ViewCompat.setNestedScrollingEnabled(viewModel.controllerBinding.root, true)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
scope.launchUI {
viewModel.getDownloadStatusFlow()
.collect(viewModel::onStatusChange)
screenModel.getDownloadStatusFlow()
.collect(screenModel::onStatusChange)
}
scope.launchUI {
viewModel.getDownloadProgressFlow()
.collect(viewModel::onUpdateDownloadedPages)
screenModel.getDownloadProgressFlow()
.collect(screenModel::onUpdateDownloadedPages)
}
viewModel.controllerBinding.root
screenModel.controllerBinding.root
},
update = {
viewModel.controllerBinding.root
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
@@ -278,7 +278,7 @@ object DownloadQueueScreen : Screen() {
bottom = bottom,
)
viewModel.adapter?.updateDataSet(downloadList)
screenModel.adapter?.updateDataSet(downloadList)
},
)
}
@@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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 DownloadQueueViewModel(
class DownloadQueueScreenModel(
private val downloadManager: DownloadManager = Injekt.get(),
) : ViewModel() {
) : ScreenModel {
private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
val state = _state.asStateFlow()
@@ -117,7 +117,7 @@ class DownloadQueueViewModel(
}
init {
viewModelScope.launch {
screenModelScope.launch {
downloadManager.queueState
.map { downloads ->
downloads
@@ -132,7 +132,7 @@ class DownloadQueueViewModel(
}
}
override fun onCleared() {
override fun onDispose() {
for (job in progressJobs.values) {
job.cancel()
}
@@ -141,7 +141,7 @@ class DownloadQueueViewModel(
}
val isDownloaderRunning = downloadManager.isDownloaderRunning
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
.stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false)
fun getDownloadStatusFlow() = downloadManager.statusFlow()
fun getDownloadProgressFlow() = downloadManager.progressFlow()
@@ -211,7 +211,7 @@ class DownloadQueueViewModel(
* @param download the download to observe its progress.
*/
private fun launchProgressJob(download: Download) {
val job = viewModelScope.launch {
val job = screenModelScope.launch {
while (download.pages == null) {
delay(50.milliseconds)
}
@@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.ui.history
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.track.interactor.AddTracks
@@ -20,7 +21,6 @@ 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.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
@@ -43,7 +43,7 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HistoryViewModel(
class HistoryScreenModel(
private val addTracks: AddTracks = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
@@ -56,13 +56,13 @@ class HistoryViewModel(
private val updateManga: UpdateManga = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
private val sourceManager: SourceManager = Injekt.get(),
) : StateViewModel<HistoryViewModel.State>(State()) {
) : StateScreenModel<HistoryScreenModel.State>(State()) {
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow()
init {
viewModelScope.launch {
screenModelScope.launch {
state.map { it.searchQuery }
.distinctUntilChanged()
.flatMapLatest { query ->
@@ -97,7 +97,7 @@ class HistoryViewModel(
}
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
viewModelScope.launchIO {
screenModelScope.launchIO {
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
}
}
@@ -108,19 +108,19 @@ class HistoryViewModel(
}
fun removeFromHistory(history: HistoryWithRelations) {
viewModelScope.launchIO {
screenModelScope.launchIO {
removeHistory.await(history)
}
}
fun removeAllFromHistory(mangaId: Long) {
viewModelScope.launchIO {
screenModelScope.launchIO {
removeHistory.await(mangaId)
}
}
fun removeAllHistory() {
viewModelScope.launchIO {
screenModelScope.launchIO {
val result = removeHistory.awaitAll()
if (!result) return@launchIO
_events.send(Event.HistoryCleared)
@@ -150,7 +150,7 @@ class HistoryViewModel(
}
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
viewModelScope.launchIO {
screenModelScope.launchIO {
setMangaCategories.await(mangaId, categoryIds)
}
}
@@ -159,7 +159,7 @@ class HistoryViewModel(
moveMangaToCategory(manga.id, categories)
if (manga.favorite) return
viewModelScope.launchIO {
screenModelScope.launchIO {
updateManga.awaitUpdateFavorite(manga.id, true)
}
}
@@ -170,7 +170,7 @@ class HistoryViewModel(
}
fun addFavorite(mangaId: Long) {
viewModelScope.launchIO {
screenModelScope.launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO
val duplicates = getDuplicateLibraryManga(manga)
@@ -184,7 +184,7 @@ class HistoryViewModel(
}
fun addFavorite(manga: Manga) {
viewModelScope.launchIO {
screenModelScope.launchIO {
// Move to default category if applicable
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory.get().toLong()
@@ -221,7 +221,7 @@ class HistoryViewModel(
}
fun showChangeCategoryDialog(manga: Manga) {
viewModelScope.launch {
screenModelScope.launch {
val categories = getCategories()
val selection = getMangaCategoryIds(manga)
mutableState.update { currentState ->

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