mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Use Stable interface for Browse screens (#7544)
This commit is contained in:
		| @@ -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 | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<ExtensionSourceItem> | ||||
| } | ||||
|  | ||||
| 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<ExtensionSourceItem> by mutableStateOf(emptyList()) | ||||
| } | ||||
| @@ -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<FilterUiModel>, | ||||
|     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(), | ||||
| @@ -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<FilterUiModel> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun ExtensionFilterState(): ExtensionFilterState { | ||||
|     return ExtensionFilterStateImpl() | ||||
| } | ||||
|  | ||||
| class ExtensionFilterStateImpl : ExtensionFilterState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<FilterUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<ExtensionUiModel>, | ||||
|     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 | ||||
|   | ||||
| @@ -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<ExtensionUiModel> | ||||
|     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<ExtensionUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<Manga>, | ||||
|     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, | ||||
|   | ||||
| @@ -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<Manga> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun MigrationMangaState(): MigrateMangaState { | ||||
|     return MigrateMangaStateImpl() | ||||
| } | ||||
|  | ||||
| class MigrateMangaStateImpl : MigrateMangaState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<Manga> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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<Pair<Source, Long>> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun MigrateSourceState(): MigrateSourceState { | ||||
|     return MigrateSourceStateImpl() | ||||
| } | ||||
|  | ||||
| class MigrateSourceStateImpl : MigrateSourceState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<FilterUiModel>, | ||||
|     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" | ||||
|   | ||||
| @@ -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<FilterUiModel> | ||||
|     val isEmpty: Boolean | ||||
| } | ||||
|  | ||||
| fun SourcesFilterState(): SourcesFilterState { | ||||
|     return SourcesFilterStateImpl() | ||||
| } | ||||
|  | ||||
| class SourcesFilterStateImpl : SourcesFilterState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var items: List<FilterUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<SourceUiModel>, | ||||
|     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<Source?>(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 }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<SourceUiModel> | ||||
|     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<SourceUiModel> by mutableStateOf(emptyList()) | ||||
|     override val isEmpty: Boolean by derivedStateOf { items.isEmpty() } | ||||
| } | ||||
| @@ -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<Extension.Installed>() | ||||
|         private set(value) { | ||||
|             field = value | ||||
|             installedExtensionsFlow.value = field | ||||
|             installedExtensionsRelay.call(value) | ||||
|         } | ||||
|  | ||||
|     private val installedExtensionsFlow = MutableStateFlow(installedExtensions) | ||||
|  | ||||
|     fun getInstalledExtensionsFlow(): StateFlow<List<Extension.Installed>> { | ||||
|         return installedExtensionsFlow.asStateFlow() | ||||
|     } | ||||
|  | ||||
|     fun getAppIconForSource(source: Source): Drawable? { | ||||
|         return getAppIconForSource(source.id) | ||||
|     } | ||||
|   | ||||
| @@ -17,9 +17,6 @@ class ExtensionFilterController : ComposeController<ExtensionFilterPresenter>() | ||||
|         ExtensionFilterScreen( | ||||
|             nestedScrollInterop = nestedScrollInterop, | ||||
|             presenter = presenter, | ||||
|             onClickLang = { language -> | ||||
|                 presenter.toggleLanguage(language) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<ExtensionFilterController>() { | ||||
| ) : BasePresenter<ExtensionFilterController>(), ExtensionFilterState by state { | ||||
|  | ||||
|     private val _state: MutableStateFlow<ExtensionFilterState> = MutableStateFlow(ExtensionFilterState.Loading) | ||||
|     val state: StateFlow<ExtensionFilterState> = _state.asStateFlow() | ||||
|     private val _events = Channel<Event>(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<String>) { | ||||
|         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<FilterUiModel>) : ExtensionFilterState() | ||||
|     sealed class Event { | ||||
|         object FailedFetchingLanguages : Event() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<ExtensionsController>() { | ||||
| ) : BasePresenter<ExtensionsController>(), ExtensionsState by state { | ||||
|  | ||||
|     private val _query: MutableStateFlow<String> = MutableStateFlow("") | ||||
|  | ||||
|     private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) | ||||
|  | ||||
|     private val _state: MutableStateFlow<ExtensionState> = MutableStateFlow(ExtensionState.Uninitialized) | ||||
|     val state: StateFlow<ExtensionState> = _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<ExtensionUiModel>) : ExtensionState() | ||||
| } | ||||
|   | ||||
| @@ -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) }, | ||||
|         ) | ||||
|   | ||||
| @@ -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<ExtensionDetailsController>() { | ||||
|  | ||||
|     val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } | ||||
|  | ||||
|     private val _state: MutableStateFlow<List<ExtensionSourceItem>> = MutableStateFlow(emptyList()) | ||||
|     val sourcesState: StateFlow<List<ExtensionSourceItem>> = _state.asStateFlow() | ||||
| ) : BasePresenter<ExtensionDetailsController>(), 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) | ||||
|     } | ||||
|   | ||||
| @@ -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<MigrationMangaController>() { | ||||
| ) : BasePresenter<MigrationMangaController>(), MigrateMangaState by state { | ||||
| 
 | ||||
|     private val _state: MutableStateFlow<MigrateMangaState> = MutableStateFlow(MigrateMangaState.Loading) | ||||
|     val state: StateFlow<MigrateMangaState> = _state.asStateFlow() | ||||
|     private val _events = Channel<Event>(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<Manga>) : MigrateMangaState() | ||||
|     sealed class Event { | ||||
|         object FailedFetchingFavorites : Event() | ||||
|     } | ||||
| } | ||||
| @@ -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<MigrationMangaPresenter> { | ||||
| class MigrationMangaController : ComposeController<MigrateMangaPresenter> { | ||||
|  | ||||
|     constructor(sourceId: Long, sourceName: String?) : super( | ||||
|         bundleOf( | ||||
| @@ -30,7 +30,7 @@ class MigrationMangaController : ComposeController<MigrationMangaPresenter> { | ||||
|  | ||||
|     override fun getTitle(): String? = sourceName | ||||
|  | ||||
|     override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId) | ||||
|     override fun createPresenter(): MigrateMangaPresenter = MigrateMangaPresenter(sourceId) | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { | ||||
|   | ||||
| @@ -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<MigrationSourcesPresenter>() { | ||||
| @@ -34,10 +33,6 @@ class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>( | ||||
|                     ), | ||||
|                 ) | ||||
|             }, | ||||
|             onLongClickItem = { source -> | ||||
|                 val sourceId = source.id.toString() | ||||
|                 activity?.copyToClipboard(sourceId, sourceId) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<MigrationSourcesController>() { | ||||
| ) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state { | ||||
|  | ||||
|     private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.Loading) | ||||
|     val state: StateFlow<MigrateSourceState> = _state.asStateFlow() | ||||
|     private val _channel = Channel<Event>(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<Pair<Source, Long>>) : MigrateSourceState() | ||||
|     sealed class Event { | ||||
|         object FailedFetchingSourcesWithCount : Event() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<SourceFilterController>() { | ||||
| ) : BasePresenter<SourceFilterController>(), SourcesFilterState by state { | ||||
|  | ||||
|     private val _state: MutableStateFlow<SourceFilterState> = MutableStateFlow(SourceFilterState.Loading) | ||||
|     val state: StateFlow<SourceFilterState> = _state.asStateFlow() | ||||
|     private val _events = Channel<Event>(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<String, List<Source>>) { | ||||
|         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<FilterUiModel>) : SourceFilterState() | ||||
|     sealed class Event { | ||||
|         object FailedFetchingLanguages : Event() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<SourcesController>() { | ||||
| ) : BasePresenter<SourcesController>(), SourcesState by state { | ||||
|  | ||||
|     private val _state: MutableStateFlow<SourceState> = MutableStateFlow(SourceState.Loading) | ||||
|     val state: StateFlow<SourceState> = _state.asStateFlow() | ||||
|     private val _events = Channel<Event>(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<SourceUiModel>) : SourceState() | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user