From 018ca71336c893f64e0d0653b48b3d5f39b88d63 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 16 Jul 2022 20:44:37 +0200 Subject: [PATCH] Use Stable interface for Browse screens (#7544) --- .../browse/ExtensionDetailsScreen.kt | 108 +++++++++--------- .../browse/ExtensionDetailsState.kt | 25 ++++ ...lterScreen.kt => ExtensionFilterScreen.kt} | 45 ++++---- .../browse/ExtensionFilterState.kt | 25 ++++ .../presentation/browse/ExtensionsScreen.kt | 22 ++-- .../presentation/browse/ExtensionsState.kt | 25 ++++ .../presentation/browse/MigrateMangaScreen.kt | 43 ++++--- .../presentation/browse/MigrateMangaState.kt | 23 ++++ .../browse/MigrateSourceScreen.kt | 27 ++--- .../presentation/browse/MigrateSourceState.kt | 23 ++++ .../browse/SourcesFilterScreen.kt | 38 +++--- .../presentation/browse/SourcesFilterState.kt | 23 ++++ .../presentation/browse/SourcesScreen.kt | 72 ++++++------ .../presentation/browse/SourcesState.kt | 27 +++++ .../tachiyomi/extension/ExtensionManager.kt | 10 ++ .../extension/ExtensionFilterController.kt | 3 - .../extension/ExtensionFilterPresenter.kt | 31 ++--- .../browse/extension/ExtensionsPresenter.kt | 35 ++---- .../details/ExtensionDetailsController.kt | 1 - .../details/ExtensionDetailsPresenter.kt | 67 +++++------ ...aPresenter.kt => MigrateMangaPresenter.kt} | 34 +++--- .../manga/MigrationMangaController.kt | 4 +- .../sources/MigrationSourcesController.kt | 5 - .../sources/MigrationSourcesPresenter.kt | 31 ++--- .../browse/source/SourcesFilterPresenter.kt | 31 ++--- .../ui/browse/source/SourcesPresenter.kt | 34 +++--- 26 files changed, 505 insertions(+), 307 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt rename app/src/main/java/eu/kanade/presentation/browse/{ExtensionLangFilterScreen.kt => ExtensionFilterScreen.kt} (71%) create mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/MigrateMangaState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/SourcesState.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/{MigrationMangaPresenter.kt => MigrateMangaPresenter.kt} (51%) diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 013f299ff..005284aa5 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -1,5 +1,8 @@ package eu.kanade.presentation.browse +import android.content.Intent +import android.net.Uri +import android.provider.Settings import android.util.DisplayMetrics import androidx.annotation.StringRes import androidx.compose.foundation.background @@ -32,7 +35,6 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -51,6 +53,7 @@ import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.components.DIVIDER_ALPHA import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.util.horizontalPadding @@ -66,65 +69,68 @@ fun ExtensionDetailsScreen( nestedScrollInterop: NestedScrollConnection, presenter: ExtensionDetailsPresenter, onClickUninstall: () -> Unit, - onClickAppInfo: () -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit, ) { - val extension = presenter.extension + when { + presenter.isLoading -> LoadingScreen() + presenter.extension == null -> EmptyScreen(textResource = R.string.empty_screen) + else -> { + val context = LocalContext.current + val extension = presenter.extension + var showNsfwWarning by remember { mutableStateOf(false) } - if (extension == null) { - EmptyScreen(textResource = R.string.empty_screen) - return - } - - val sources by presenter.sourcesState.collectAsState() - - var showNsfwWarning by remember { mutableStateOf(false) } - - ScrollbarLazyColumn( - modifier = Modifier.nestedScroll(nestedScrollInterop), - contentPadding = WindowInsets.navigationBars.asPaddingValues(), - ) { - when { - extension.isUnofficial -> - item { - WarningBanner(R.string.unofficial_extension_message) + ScrollbarLazyColumn( + modifier = Modifier.nestedScroll(nestedScrollInterop), + contentPadding = WindowInsets.navigationBars.asPaddingValues(), + ) { + when { + extension.isUnofficial -> + item { + WarningBanner(R.string.unofficial_extension_message) + } + extension.isObsolete -> + item { + WarningBanner(R.string.obsolete_extension_message) + } } - extension.isObsolete -> + item { - WarningBanner(R.string.obsolete_extension_message) + DetailsHeader( + extension = extension, + onClickUninstall = onClickUninstall, + onClickAppInfo = { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", extension.pkgName, null) + context.startActivity(this) + } + }, + onClickAgeRating = { + showNsfwWarning = true + }, + ) } - } - item { - DetailsHeader( - extension = extension, - onClickUninstall = onClickUninstall, - onClickAppInfo = onClickAppInfo, - onClickAgeRating = { - showNsfwWarning = true - }, - ) + items( + items = presenter.sources, + key = { it.source.id }, + ) { source -> + SourceSwitchPreference( + modifier = Modifier.animateItemPlacement(), + source = source, + onClickSourcePreferences = onClickSourcePreferences, + onClickSource = onClickSource, + ) + } + } + if (showNsfwWarning) { + NsfwWarningDialog( + onClickConfirm = { + showNsfwWarning = false + }, + ) + } } - - items( - items = sources, - key = { it.source.id }, - ) { source -> - SourceSwitchPreference( - modifier = Modifier.animateItemPlacement(), - source = source, - onClickSourcePreferences = onClickSourcePreferences, - onClickSource = onClickSource, - ) - } - } - if (showNsfwWarning) { - NsfwWarningDialog( - onClickConfirm = { - showNsfwWarning = false - }, - ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt new file mode 100644 index 000000000..1b229e272 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsState.kt @@ -0,0 +1,25 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem + +@Stable +interface ExtensionDetailsState { + val isLoading: Boolean + val extension: Extension.Installed? + val sources: List +} + +fun ExtensionDetailsState(): ExtensionDetailsState { + return ExtensionDetailsStateImpl() +} + +class ExtensionDetailsStateImpl : ExtensionDetailsState { + override var isLoading: Boolean by mutableStateOf(true) + override var extension: Extension.Installed? by mutableStateOf(null) + override var sources: List by mutableStateOf(emptyList()) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionLangFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/browse/ExtensionLangFilterScreen.kt rename to app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt index 8532e3ba3..ee7ab65c3 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionLangFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterScreen.kt @@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.lazy.items import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -19,47 +17,52 @@ import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.PreferenceRow import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter -import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState -import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest @Composable fun ExtensionFilterScreen( nestedScrollInterop: NestedScrollConnection, presenter: ExtensionFilterPresenter, - onClickLang: (String) -> Unit, ) { - val state by presenter.state.collectAsState() - - when (state) { - is ExtensionFilterState.Loading -> LoadingScreen() - is ExtensionFilterState.Error -> Text(text = (state as ExtensionFilterState.Error).error.message!!) - is ExtensionFilterState.Success -> + val context = LocalContext.current + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen) + else -> { SourceFilterContent( nestedScrollInterop = nestedScrollInterop, - items = (state as ExtensionFilterState.Success).models, - onClickLang = onClickLang, + state = presenter, + onClickLang = { + presenter.toggleLanguage(it) + }, ) + } + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { + when (it) { + ExtensionFilterPresenter.Event.FailedFetchingLanguages -> { + context.toast(R.string.internal_error) + } + } + } } } @Composable fun SourceFilterContent( nestedScrollInterop: NestedScrollConnection, - items: List, + state: ExtensionFilterState, onClickLang: (String) -> Unit, ) { - if (items.isEmpty()) { - EmptyScreen(textResource = R.string.empty_screen) - return - } - LazyColumn( modifier = Modifier.nestedScroll(nestedScrollInterop), contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { items( - items = items, + items = state.items, ) { model -> ExtensionFilterItem( modifier = Modifier.animateItemPlacement(), diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt new file mode 100644 index 000000000..b5d03ca87 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionFilterState.kt @@ -0,0 +1,25 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel + +@Stable +interface ExtensionFilterState { + val isLoading: Boolean + val items: List + val isEmpty: Boolean +} + +fun ExtensionFilterState(): ExtensionFilterState { + return ExtensionFilterStateImpl() +} + +class ExtensionFilterStateImpl : ExtensionFilterState { + override var isLoading: Boolean by mutableStateOf(true) + override var items: List by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index 73a3425c5..2ea39e642 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -40,7 +39,9 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.ExtensionIcon +import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.FastScrollLazyColumn +import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.theme.header import eu.kanade.presentation.util.horizontalPadding @@ -49,7 +50,6 @@ import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep -import eu.kanade.tachiyomi.ui.browse.extension.ExtensionState import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter import eu.kanade.tachiyomi.util.system.LocaleHelper @@ -69,19 +69,18 @@ fun ExtensionScreen( onRefresh: () -> Unit, onLaunched: () -> Unit, ) { - val state by presenter.state.collectAsState() - val isRefreshing = presenter.isRefreshing - SwipeRefresh( modifier = Modifier.nestedScroll(nestedScrollInterop), - state = rememberSwipeRefreshState(isRefreshing), + state = rememberSwipeRefreshState(presenter.isRefreshing), indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) }, onRefresh = onRefresh, ) { - when (state) { - is ExtensionState.Initialized -> { + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(R.string.empty_screen) + else -> { ExtensionContent( - items = (state as ExtensionState.Initialized).list, + state = presenter, onLongClickItem = onLongClickItem, onClickItemCancel = onClickItemCancel, onInstallExtension = onInstallExtension, @@ -93,14 +92,13 @@ fun ExtensionScreen( onLaunched = onLaunched, ) } - ExtensionState.Uninitialized -> {} } } } @Composable fun ExtensionContent( - items: List, + state: ExtensionsState, onLongClickItem: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit, onInstallExtension: (Extension.Available) -> Unit, @@ -117,7 +115,7 @@ fun ExtensionContent( contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { items( - items = items, + items = state.items, key = { when (it) { is ExtensionUiModel.Header.Resource -> it.textRes diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt new file mode 100644 index 000000000..7e629f90d --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsState.kt @@ -0,0 +1,25 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel + +interface ExtensionsState { + val isLoading: Boolean + val isRefreshing: Boolean + val items: List + val isEmpty: Boolean +} + +fun ExtensionState(): ExtensionsState { + return ExtensionsStateImpl() +} + +class ExtensionsStateImpl : ExtensionsState { + override var isLoading: Boolean by mutableStateOf(true) + override var isRefreshing: Boolean by mutableStateOf(false) + override var items: List by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt index 6dd5e4298..1a64d7d9d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt @@ -4,61 +4,66 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import eu.kanade.domain.manga.model.Manga import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.manga.components.BaseMangaListItem import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState -import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaPresenter +import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter +import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest @Composable fun MigrateMangaScreen( nestedScrollInterop: NestedScrollConnection, - presenter: MigrationMangaPresenter, + presenter: MigrateMangaPresenter, onClickItem: (Manga) -> Unit, onClickCover: (Manga) -> Unit, ) { - val state by presenter.state.collectAsState() - - when (state) { - MigrateMangaState.Loading -> LoadingScreen() - is MigrateMangaState.Error -> Text(text = (state as MigrateMangaState.Error).error.message!!) - is MigrateMangaState.Success -> { + val context = LocalContext.current + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen) + else -> { MigrateMangaContent( nestedScrollInterop = nestedScrollInterop, - list = (state as MigrateMangaState.Success).list, + state = presenter, onClickItem = onClickItem, onClickCover = onClickCover, ) } } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + Event.FailedFetchingFavorites -> { + context.toast(R.string.internal_error) + } + } + } + } } @Composable fun MigrateMangaContent( nestedScrollInterop: NestedScrollConnection, - list: List, + state: MigrateMangaState, onClickItem: (Manga) -> Unit, onClickCover: (Manga) -> Unit, ) { - if (list.isEmpty()) { - EmptyScreen(textResource = R.string.empty_screen) - return - } ScrollbarLazyColumn( modifier = Modifier.nestedScroll(nestedScrollInterop), contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { - items(list) { manga -> + items(state.items) { manga -> MigrateMangaItem( manga = manga, onClickItem = onClickItem, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaState.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaState.kt new file mode 100644 index 000000000..b4cf8d0ce --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaState.kt @@ -0,0 +1,23 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.manga.model.Manga + +interface MigrateMangaState { + val isLoading: Boolean + val items: List + val isEmpty: Boolean +} + +fun MigrationMangaState(): MigrateMangaState { + return MigrateMangaStateImpl() +} + +class MigrateMangaStateImpl : MigrateMangaState { + override var isLoading: Boolean by mutableStateOf(true) + override var items: List by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index c81f1965f..efb9579b2 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -11,12 +11,12 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -32,27 +32,29 @@ import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.copyToClipboard @Composable fun MigrateSourceScreen( nestedScrollInterop: NestedScrollConnection, presenter: MigrationSourcesPresenter, onClickItem: (Source) -> Unit, - onLongClickItem: (Source) -> Unit, ) { - val state by presenter.state.collectAsState() - when (state) { - is MigrateSourceState.Loading -> LoadingScreen() - is MigrateSourceState.Error -> Text(text = (state as MigrateSourceState.Error).error.message!!) - is MigrateSourceState.Success -> + val context = LocalContext.current + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) + else -> MigrateSourceList( nestedScrollInterop = nestedScrollInterop, - list = (state as MigrateSourceState.Success).sources, + list = presenter.items, onClickItem = onClickItem, - onLongClickItem = onLongClickItem, + onLongClickItem = { source -> + val sourceId = source.id.toString() + context.copyToClipboard(sourceId, sourceId) + }, ) } } @@ -64,11 +66,6 @@ fun MigrateSourceList( onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, ) { - if (list.isEmpty()) { - EmptyScreen(textResource = R.string.information_empty_library) - return - } - ScrollbarLazyColumn( modifier = Modifier.nestedScroll(nestedScrollInterop), contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt new file mode 100644 index 000000000..2a7f4cc9b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceState.kt @@ -0,0 +1,23 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.domain.source.model.Source + +interface MigrateSourceState { + val isLoading: Boolean + val items: List> + val isEmpty: Boolean +} + +fun MigrateSourceState(): MigrateSourceState { + return MigrateSourceStateImpl() +} + +class MigrateSourceStateImpl : MigrateSourceState { + override var isLoading: Boolean by mutableStateOf(true) + override var items: List> by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt index 097a76264..c0bcfd97a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterScreen.kt @@ -6,9 +6,8 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.lazy.items import androidx.compose.material3.Checkbox import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -22,9 +21,10 @@ import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel -import eu.kanade.tachiyomi.ui.browse.source.SourceFilterState import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest @Composable fun SourcesFilterScreen( @@ -33,39 +33,43 @@ fun SourcesFilterScreen( onClickLang: (String) -> Unit, onClickSource: (Source) -> Unit, ) { - val state by presenter.state.collectAsState() - - when (state) { - is SourceFilterState.Loading -> LoadingScreen() - is SourceFilterState.Error -> Text(text = (state as SourceFilterState.Error).error.message!!) - is SourceFilterState.Success -> + val context = LocalContext.current + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen) + else -> { SourcesFilterContent( nestedScrollInterop = nestedScrollInterop, - items = (state as SourceFilterState.Success).models, + state = presenter, onClickLang = onClickLang, onClickSource = onClickSource, ) + } + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + SourcesFilterPresenter.Event.FailedFetchingLanguages -> { + context.toast(R.string.internal_error) + } + } + } } } @Composable fun SourcesFilterContent( nestedScrollInterop: NestedScrollConnection, - items: List, + state: SourcesFilterState, onClickLang: (String) -> Unit, onClickSource: (Source) -> Unit, ) { - if (items.isEmpty()) { - EmptyScreen(textResource = R.string.source_filter_empty_screen) - return - } - ScrollbarLazyColumn( modifier = Modifier.nestedScroll(nestedScrollInterop), contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { items( - items = items, + items = state.items, contentType = { when (it) { is FilterUiModel.Header -> "header" diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt new file mode 100644 index 000000000..46668bcf7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesFilterState.kt @@ -0,0 +1,23 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel + +interface SourcesFilterState { + val isLoading: Boolean + val items: List + val isEmpty: Boolean +} + +fun SourcesFilterState(): SourcesFilterState { + return SourcesFilterStateImpl() +} + +class SourcesFilterStateImpl : SourcesFilterState { + override var isLoading: Boolean by mutableStateOf(true) + override var items: List by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index fc311b28e..8eb584b28 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -19,10 +19,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -42,9 +40,11 @@ import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.ui.browse.source.SourceState import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter +import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.collectLatest @Composable fun SourcesScreen( @@ -55,44 +55,47 @@ fun SourcesScreen( onClickLatest: (Source) -> Unit, onClickPin: (Source) -> Unit, ) { - val state by presenter.state.collectAsState() - - when (state) { - is SourceState.Loading -> LoadingScreen() - is SourceState.Error -> Text(text = (state as SourceState.Error).error.message!!) - is SourceState.Success -> SourceList( - nestedScrollConnection = nestedScrollInterop, - list = (state as SourceState.Success).uiModels, - onClickItem = onClickItem, - onClickDisable = onClickDisable, - onClickLatest = onClickLatest, - onClickPin = onClickPin, - ) + val context = LocalContext.current + when { + presenter.isLoading -> LoadingScreen() + presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen) + else -> { + SourceList( + nestedScrollConnection = nestedScrollInterop, + state = presenter, + onClickItem = onClickItem, + onClickDisable = onClickDisable, + onClickLatest = onClickLatest, + onClickPin = onClickPin, + ) + } + } + LaunchedEffect(Unit) { + presenter.events.collectLatest { event -> + when (event) { + SourcesPresenter.Event.FailedFetchingSources -> { + context.toast(R.string.internal_error) + } + } + } } } @Composable fun SourceList( nestedScrollConnection: NestedScrollConnection, - list: List, + state: SourcesState, onClickItem: (Source) -> Unit, onClickDisable: (Source) -> Unit, onClickLatest: (Source) -> Unit, onClickPin: (Source) -> Unit, ) { - if (list.isEmpty()) { - EmptyScreen(textResource = R.string.source_empty_screen) - return - } - - var sourceState by remember { mutableStateOf(null) } - ScrollbarLazyColumn( modifier = Modifier.nestedScroll(nestedScrollConnection), contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, ) { items( - items = list, + items = state.items, contentType = { when (it) { is SourceUiModel.Header -> "header" @@ -117,7 +120,7 @@ fun SourceList( modifier = Modifier.animateItemPlacement(), source = model.source, onClickItem = onClickItem, - onLongClickItem = { sourceState = it }, + onLongClickItem = { state.dialog = Dialog(it) }, onClickLatest = onClickLatest, onClickPin = onClickPin, ) @@ -125,18 +128,19 @@ fun SourceList( } } - if (sourceState != null) { + if (state.dialog != null) { + val source = state.dialog!!.source SourceOptionsDialog( - source = sourceState!!, + source = source, onClickPin = { - onClickPin(sourceState!!) - sourceState = null + onClickPin(source) + state.dialog = null }, onClickDisable = { - onClickDisable(sourceState!!) - sourceState = null + onClickDisable(source) + state.dialog = null }, - onDismiss = { sourceState = null }, + onDismiss = { state.dialog = null }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesState.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesState.kt new file mode 100644 index 000000000..87894a229 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesState.kt @@ -0,0 +1,27 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter + +@Stable +interface SourcesState { + var dialog: SourcesPresenter.Dialog? + val isLoading: Boolean + val items: List + val isEmpty: Boolean +} + +fun SourcesState(): SourcesState { + return SourcesStateImpl() +} + +class SourcesStateImpl : SourcesState { + override var dialog: SourcesPresenter.Dialog? by mutableStateOf(null) + override var isLoading: Boolean by mutableStateOf(true) + override var items: List by mutableStateOf(emptyList()) + override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index f8e036821..9148c839c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -20,6 +20,9 @@ import eu.kanade.tachiyomi.util.preference.plusAssign import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import logcat.LogPriority import rx.Observable import uy.kohesive.injekt.Injekt @@ -63,9 +66,16 @@ class ExtensionManager( var installedExtensions = emptyList() private set(value) { field = value + installedExtensionsFlow.value = field installedExtensionsRelay.call(value) } + private val installedExtensionsFlow = MutableStateFlow(installedExtensions) + + fun getInstalledExtensionsFlow(): StateFlow> { + return installedExtensionsFlow.asStateFlow() + } + fun getAppIconForSource(source: Source): Drawable? { return getAppIconForSource(source.id) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt index 2254da3ae..deee79081 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterController.kt @@ -17,9 +17,6 @@ class ExtensionFilterController : ComposeController() ExtensionFilterScreen( nestedScrollInterop = nestedScrollInterop, presenter = presenter, - onClickLang = { language -> - presenter.toggleLanguage(language) - }, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt index c1e3aae66..63c3156f4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionFilterPresenter.kt @@ -3,32 +3,37 @@ package eu.kanade.tachiyomi.ui.browse.extension import android.os.Bundle import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.source.interactor.ToggleLanguage +import eu.kanade.presentation.browse.ExtensionFilterState +import eu.kanade.presentation.browse.ExtensionFilterStateImpl import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionFilterPresenter( + private val state: ExtensionFilterStateImpl = ExtensionFilterState() as ExtensionFilterStateImpl, private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), ExtensionFilterState by state { - private val _state: MutableStateFlow = MutableStateFlow(ExtensionFilterState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _events = Channel(Int.MAX_VALUE) + val events = _events.receiveAsFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) presenterScope.launchIO { getExtensionLanguages.subscribe() .catch { exception -> - _state.value = ExtensionFilterState.Error(exception) + logcat(LogPriority.ERROR, exception) + _events.send(Event.FailedFetchingLanguages) } .collectLatest(::collectLatestSourceLangMap) } @@ -36,19 +41,17 @@ class ExtensionFilterPresenter( private fun collectLatestSourceLangMap(extLangs: List) { val enabledLanguages = preferences.enabledLanguages().get() - val uiModels = extLangs.map { + state.items = extLangs.map { FilterUiModel(it, it in enabledLanguages) } - _state.value = ExtensionFilterState.Success(uiModels) + state.isLoading = false } fun toggleLanguage(language: String) { toggleLanguage.await(language) } -} -sealed class ExtensionFilterState { - object Loading : ExtensionFilterState() - data class Error(val error: Throwable) : ExtensionFilterState() - data class Success(val models: List) : ExtensionFilterState() + sealed class Event { + object FailedFetchingLanguages : Event() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt index 48cbe3e5f..b47f61b9d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsPresenter.kt @@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.extension import android.app.Application import android.os.Bundle import androidx.annotation.StringRes -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import eu.kanade.domain.extension.interactor.GetExtensionUpdates import eu.kanade.domain.extension.interactor.GetExtensions +import eu.kanade.presentation.browse.ExtensionState +import eu.kanade.presentation.browse.ExtensionsState +import eu.kanade.presentation.browse.ExtensionsStateImpl import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension @@ -17,8 +17,6 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.system.LocaleHelper import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update @@ -27,20 +25,16 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionsPresenter( + private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl, private val extensionManager: ExtensionManager = Injekt.get(), private val getExtensionUpdates: GetExtensionUpdates = Injekt.get(), private val getExtensions: GetExtensions = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), ExtensionsState by state { private val _query: MutableStateFlow = MutableStateFlow("") private var _currentDownloads = MutableStateFlow>(hashMapOf()) - private val _state: MutableStateFlow = MutableStateFlow(ExtensionState.Uninitialized) - val state: StateFlow = _state.asStateFlow() - - var isRefreshing: Boolean by mutableStateOf(true) - override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -86,8 +80,6 @@ class ExtensionsPresenter( getExtensionUpdates.subscribe(), _currentDownloads, ) { query, (installed, untrusted, available), updates, downloads -> - isRefreshing = false - val languagesWithExtensions = available .filter(queryFilter(query)) .groupBy { LocaleHelper.getSourceDisplayName(it.lang, context) } @@ -121,7 +113,9 @@ class ExtensionsPresenter( items }.collectLatest { - _state.value = ExtensionState.Initialized(it) + state.isRefreshing = false + state.isLoading = false + state.items = it } } } @@ -134,9 +128,9 @@ class ExtensionsPresenter( fun updateAllExtensions() { launchIO { - val state = _state.value - if (state !is ExtensionState.Initialized) return@launchIO - state.list.mapNotNull { + if (state.isEmpty) return@launchIO + val items = state.items + items.mapNotNull { if (it !is ExtensionUiModel.Item) return@mapNotNull null if (it.extension !is Extension.Installed) return@mapNotNull null if (it.extension.hasUpdate.not()) return@mapNotNull null @@ -189,7 +183,7 @@ class ExtensionsPresenter( } fun findAvailableExtensions() { - isRefreshing = true + state.isRefreshing = true extensionManager.findAvailableExtensions() } @@ -217,8 +211,3 @@ sealed interface ExtensionUiModel { } } } - -sealed class ExtensionState { - object Uninitialized : ExtensionState() - data class Initialized(val list: List) : ExtensionState() -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index e2c3ec34b..7af4ef225 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -43,7 +43,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) : nestedScrollInterop = nestedScrollInterop, presenter = presenter, onClickUninstall = { presenter.uninstallExtension() }, - onClickAppInfo = { presenter.openInSettings() }, onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) }, onClickSource = { presenter.toggleSource(it) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt index 0a0eecd13..621806e4d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsPresenter.kt @@ -1,48 +1,52 @@ package eu.kanade.tachiyomi.ui.browse.extension.details import android.app.Application -import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.provider.Settings import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.source.interactor.ToggleSource +import eu.kanade.presentation.browse.ExtensionDetailsState +import eu.kanade.presentation.browse.ExtensionDetailsStateImpl import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.system.LocaleHelper -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import rx.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.take import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionDetailsPresenter( private val pkgName: String, + private val state: ExtensionDetailsStateImpl = ExtensionDetailsState() as ExtensionDetailsStateImpl, private val context: Application = Injekt.get(), private val getExtensionSources: GetExtensionSources = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), -) : BasePresenter() { - - val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } - - private val _state: MutableStateFlow> = MutableStateFlow(emptyList()) - val sourcesState: StateFlow> = _state.asStateFlow() +) : BasePresenter(), ExtensionDetailsState by state { override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - val extension = extension ?: return + presenterScope.launchIO { + extensionManager.getInstalledExtensionsFlow() + .map { it.firstOrNull { it.pkgName == pkgName } } + .collectLatest { + state.extension = it + fetchExtensionSources() + } + } bindToUninstalledExtension() + } - presenterScope.launchIO { - getExtensionSources.subscribe(extension) + private fun CoroutineScope.fetchExtensionSources() { + launchIO { + getExtensionSources.subscribe(extension!!) .map { it.sortedWith( compareBy( @@ -51,20 +55,24 @@ class ExtensionDetailsPresenter( ), ) } - .collectLatest { _state.value = it } + .collectLatest { + state.isLoading = false + state.sources = it + } } } private fun bindToUninstalledExtension() { - extensionManager.getInstalledExtensionsObservable() - .skip(1) - .filter { extensions -> extensions.none { it.pkgName == pkgName } } - .map { } - .take(1) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onExtensionUninstalled() - },) + presenterScope.launchIO { + extensionManager.getInstalledExtensionsFlow() + .drop(1) + .filter { extensions -> extensions.none { it.pkgName == pkgName } } + .map { } + .take(1) + .collectLatest { + view?.onExtensionUninstalled() + } + } } fun uninstallExtension() { @@ -72,13 +80,6 @@ class ExtensionDetailsPresenter( extensionManager.uninstallExtension(extension.pkgName) } - fun openInSettings() { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", pkgName, null) - } - view?.startActivity(intent) - } - fun toggleSource(sourceId: Long) { toggleSource.await(sourceId) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaPresenter.kt similarity index 51% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaPresenter.kt index 4e1bbbc29..cc34184db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaPresenter.kt @@ -2,25 +2,29 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga import android.os.Bundle import eu.kanade.domain.manga.interactor.GetFavorites -import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.MigrateMangaState +import eu.kanade.presentation.browse.MigrateMangaStateImpl +import eu.kanade.presentation.browse.MigrationMangaState import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class MigrationMangaPresenter( +class MigrateMangaPresenter( private val sourceId: Long, + private val state: MigrateMangaStateImpl = MigrationMangaState() as MigrateMangaStateImpl, private val getFavorites: GetFavorites = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), MigrateMangaState by state { - private val _state: MutableStateFlow = MutableStateFlow(MigrateMangaState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _events = Channel(Int.MAX_VALUE) + val events = _events.receiveAsFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -28,20 +32,20 @@ class MigrationMangaPresenter( getFavorites .subscribe(sourceId) .catch { exception -> - _state.value = MigrateMangaState.Error(exception) + logcat(LogPriority.ERROR, exception) + _events.send(Event.FailedFetchingFavorites) } .map { list -> list.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) } .collectLatest { sortedList -> - _state.value = MigrateMangaState.Success(sortedList) + state.isLoading = false + state.items = sortedList } } } -} -sealed class MigrateMangaState { - object Loading : MigrateMangaState() - data class Error(val error: Throwable) : MigrateMangaState() - data class Success(val list: List) : MigrateMangaState() + sealed class Event { + object FailedFetchingFavorites : Event() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt index 024acc78d..29e30ea15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt @@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.manga.MangaController -class MigrationMangaController : ComposeController { +class MigrationMangaController : ComposeController { constructor(sourceId: Long, sourceName: String?) : super( bundleOf( @@ -30,7 +30,7 @@ class MigrationMangaController : ComposeController { override fun getTitle(): String? = sourceName - override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId) + override fun createPresenter(): MigrateMangaPresenter = MigrateMangaPresenter(sourceId) @Composable override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt index 4e1762f16..dde0a1a76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController -import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser class MigrationSourcesController : ComposeController() { @@ -34,10 +33,6 @@ class MigrationSourcesController : ComposeController( ), ) }, - onLongClickItem = { source -> - val sourceId = source.id.toString() - activity?.copyToClipboard(sourceId, sourceId) - }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt index 904ade749..6beb37980 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt @@ -3,24 +3,27 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources import android.os.Bundle import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.SetMigrateSorting -import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.browse.MigrateSourceState +import eu.kanade.presentation.browse.MigrateSourceStateImpl import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MigrationSourcesPresenter( + private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl, private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), MigrateSourceState by state { - private val _state: MutableStateFlow = MutableStateFlow(MigrateSourceState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _channel = Channel(Int.MAX_VALUE) + val channel = _channel.receiveAsFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -28,10 +31,12 @@ class MigrationSourcesPresenter( presenterScope.launchIO { getSourcesWithFavoriteCount.subscribe() .catch { exception -> - _state.value = MigrateSourceState.Error(exception) + logcat(LogPriority.ERROR, exception) + _channel.send(Event.FailedFetchingSourcesWithCount) } .collectLatest { sources -> - _state.value = MigrateSourceState.Success(sources) + state.items = sources + state.isLoading = false } } } @@ -43,10 +48,8 @@ class MigrationSourcesPresenter( fun setTotalSorting(isAscending: Boolean) { setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) } -} -sealed class MigrateSourceState { - object Loading : MigrateSourceState() - data class Error(val error: Throwable) : MigrateSourceState() - data class Success(val sources: List>) : MigrateSourceState() + sealed class Event { + object FailedFetchingSourcesWithCount : Event() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt index d8032d15e..06afa87dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesFilterPresenter.kt @@ -5,26 +5,30 @@ import eu.kanade.domain.source.interactor.GetLanguagesWithSources import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.browse.SourcesFilterState +import eu.kanade.presentation.browse.SourcesFilterStateImpl import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SourcesFilterPresenter( + private val state: SourcesFilterStateImpl = SourcesFilterState() as SourcesFilterStateImpl, private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), SourcesFilterState by state { - private val _state: MutableStateFlow = MutableStateFlow(SourceFilterState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _events = Channel(Int.MAX_VALUE) + val events = _events.receiveAsFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -32,14 +36,15 @@ class SourcesFilterPresenter( presenterScope.launchIO { getLanguagesWithSources.subscribe() .catch { exception -> - _state.value = SourceFilterState.Error(exception) + logcat(LogPriority.ERROR, exception) + _events.send(Event.FailedFetchingLanguages) } .collectLatest(::collectLatestSourceLangMap) } } private fun collectLatestSourceLangMap(sourceLangMap: Map>) { - val uiModels = sourceLangMap.flatMap { + state.items = sourceLangMap.flatMap { val isLangEnabled = it.key in preferences.enabledLanguages().get() val header = listOf(FilterUiModel.Header(it.key, isLangEnabled)) @@ -51,7 +56,7 @@ class SourcesFilterPresenter( ) } } - _state.value = SourceFilterState.Success(uiModels) + state.isLoading = false } fun toggleSource(source: Source) { @@ -61,10 +66,8 @@ class SourcesFilterPresenter( fun toggleLanguage(language: String) { toggleLanguage.await(language) } -} -sealed class SourceFilterState { - object Loading : SourceFilterState() - data class Error(val error: Throwable) : SourceFilterState() - data class Success(val models: List) : SourceFilterState() + sealed class Event { + object FailedFetchingLanguages : Event() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt index b1e6cb38b..c26c748ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesPresenter.kt @@ -7,32 +7,37 @@ import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Source import eu.kanade.presentation.browse.SourceUiModel +import eu.kanade.presentation.browse.SourcesState +import eu.kanade.presentation.browse.SourcesStateImpl import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.TreeMap class SourcesPresenter( + private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl, private val getEnabledSources: GetEnabledSources = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), SourcesState by state { - private val _state: MutableStateFlow = MutableStateFlow(SourceState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _events = Channel(Int.MAX_VALUE) + val events = _events.receiveAsFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) presenterScope.launchIO { getEnabledSources.subscribe() .catch { exception -> - _state.value = SourceState.Error(exception) + logcat(LogPriority.ERROR, exception) + _events.send(Event.FailedFetchingSources) } .collectLatest(::collectLatestSources) } @@ -67,7 +72,8 @@ class SourcesPresenter( }.toTypedArray(), ) } - _state.value = SourceState.Success(uiModels) + state.isLoading = false + state.items = uiModels } fun toggleSource(source: Source) { @@ -78,14 +84,14 @@ class SourcesPresenter( toggleSourcePin.await(source) } + sealed class Event { + object FailedFetchingSources : Event() + } + + data class Dialog(val source: Source) + companion object { const val PINNED_KEY = "pinned" const val LAST_USED_KEY = "last_used" } } - -sealed class SourceState { - object Loading : SourceState() - data class Error(val error: Throwable) : SourceState() - data class Success(val uiModels: List) : SourceState() -}