From 75b23c99ecd4eef46bc528d65c5307eb44f1a346 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 10 Jan 2023 23:18:34 -0500 Subject: [PATCH] Refactor how extensions list is modelled To better enable changing the UI in the future based on sections. --- .../presentation/browse/ExtensionsScreen.kt | 132 +++++++++--------- .../browse/extension/ExtensionsScreenModel.kt | 47 +++---- 2 files changed, 85 insertions(+), 94 deletions(-) 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 e3893a2fe..c711eaed6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -146,79 +146,75 @@ private fun ExtensionContent( } } - items( - items = state.items, - contentType = { - when (it) { - is ExtensionUiModel.Header -> "header" - is ExtensionUiModel.Item -> "item" - } - }, - key = { - when (it) { - is ExtensionUiModel.Header -> "extensionHeader-${it.hashCode()}" - is ExtensionUiModel.Item -> "extension-${it.hashCode()}" - } - }, - ) { item -> - when (item) { - is ExtensionUiModel.Header.Resource -> { - val action: @Composable RowScope.() -> Unit = - if (item.textRes == R.string.ext_updates_pending) { - { - Button(onClick = { onClickUpdateAll() }) { - Text( - text = stringResource(R.string.ext_update_all), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onPrimary, - ), - ) - } - } - } else { - {} - } - ExtensionHeader( - textRes = item.textRes, - modifier = Modifier.animateItemPlacement(), - action = action, - ) - } - is ExtensionUiModel.Header.Text -> { - ExtensionHeader( - text = item.text, - modifier = Modifier.animateItemPlacement(), - ) - } - is ExtensionUiModel.Item -> { - ExtensionItem( - modifier = Modifier.animateItemPlacement(), - item = item, - onClickItem = { - when (it) { - is Extension.Available -> onInstallExtension(it) - is Extension.Installed -> onOpenExtension(it) - is Extension.Untrusted -> { trustState = it } - } - }, - onLongClickItem = onLongClickItem, - onClickItemCancel = onClickItemCancel, - onClickItemAction = { - when (it) { - is Extension.Available -> onInstallExtension(it) - is Extension.Installed -> { - if (it.hasUpdate) { - onUpdateExtension(it) - } else { - onOpenExtension(it) + state.items.forEach { (header, items) -> + item( + contentType = "header", + key = "extensionHeader-${header.hashCode()}", + ) { + when (header) { + is ExtensionUiModel.Header.Resource -> { + val action: @Composable RowScope.() -> Unit = + if (header.textRes == R.string.ext_updates_pending) { + { + Button(onClick = { onClickUpdateAll() }) { + Text( + text = stringResource(R.string.ext_update_all), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onPrimary, + ), + ) } } - is Extension.Untrusted -> { trustState = it } + } else { + {} } - }, - ) + ExtensionHeader( + textRes = header.textRes, + modifier = Modifier.animateItemPlacement(), + action = action, + ) + } + is ExtensionUiModel.Header.Text -> { + ExtensionHeader( + text = header.text, + modifier = Modifier.animateItemPlacement(), + ) + } } } + + items( + items = items, + contentType = { "item" }, + key = { "extension-${it.hashCode()}" }, + ) { item -> + ExtensionItem( + modifier = Modifier.animateItemPlacement(), + item = item, + onClickItem = { + when (it) { + is Extension.Available -> onInstallExtension(it) + is Extension.Installed -> onOpenExtension(it) + is Extension.Untrusted -> { trustState = it } + } + }, + onLongClickItem = onLongClickItem, + onClickItemCancel = onClickItemCancel, + onClickItemAction = { + when (it) { + is Extension.Available -> onInstallExtension(it) + is Extension.Installed -> { + if (it.hasUpdate) { + onUpdateExtension(it) + } else { + onOpenExtension(it) + } + } + is Extension.Untrusted -> { trustState = it } + } + }, + ) + } } } if (trustState != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index 184e47951..daa889b11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -40,7 +40,7 @@ class ExtensionsScreenModel( init { val context = Injekt.get() - val extensionMapper: (Map) -> ((Extension) -> ExtensionUiModel) = { map -> + val extensionMapper: (Map) -> ((Extension) -> ExtensionUiModel.Item) = { map -> { ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle) } @@ -80,38 +80,31 @@ class ExtensionsScreenModel( ) { query, downloads, (_updates, _installed, _available, _untrusted) -> val searchQuery = query ?: "" - val languagesWithExtensions = _available - .filter(queryFilter(searchQuery)) - .groupBy { it.lang } - .toSortedMap(LocaleHelper.comparator) - .flatMap { (lang, exts) -> - listOf( - ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)), - *exts.map(extensionMapper(downloads)).toTypedArray(), - ) - } - - val items = mutableListOf() + val itemsGroups: ItemGroups = mutableMapOf() val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) if (updates.isNotEmpty()) { - items.add(ExtensionUiModel.Header.Resource(R.string.ext_updates_pending)) - items.addAll(updates) + itemsGroups[ExtensionUiModel.Header.Resource(R.string.ext_updates_pending)] = updates } val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) if (installed.isNotEmpty() || untrusted.isNotEmpty()) { - items.add(ExtensionUiModel.Header.Resource(R.string.ext_installed)) - items.addAll(installed) - items.addAll(untrusted) + itemsGroups[ExtensionUiModel.Header.Resource(R.string.ext_installed)] = installed + untrusted } + val languagesWithExtensions = _available + .filter(queryFilter(searchQuery)) + .groupBy { it.lang } + .toSortedMap(LocaleHelper.comparator) + .map { (lang, exts) -> + ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to exts.map(extensionMapper(downloads)) + } if (languagesWithExtensions.isNotEmpty()) { - items.addAll(languagesWithExtensions) + itemsGroups.putAll(languagesWithExtensions) } - items + itemsGroups } .collectLatest { mutableState.update { state -> @@ -140,10 +133,10 @@ class ExtensionsScreenModel( coroutineScope.launchIO { with(state.value) { if (isEmpty) return@launchIO - items + items.values + .flatten() .mapNotNull { when { - it !is ExtensionUiModel.Item -> null it.extension !is Extension.Installed -> null !it.extension.hasUpdate -> null else -> it.extension @@ -216,14 +209,16 @@ class ExtensionsScreenModel( data class ExtensionsState( val isLoading: Boolean = true, val isRefreshing: Boolean = false, - val items: List = emptyList(), + val items: ItemGroups = mutableMapOf(), val updates: Int = 0, ) { val isEmpty = items.isEmpty() } -sealed interface ExtensionUiModel { - sealed interface Header : ExtensionUiModel { +typealias ItemGroups = MutableMap> + +object ExtensionUiModel { + sealed interface Header { data class Resource(@StringRes val textRes: Int) : Header data class Text(val text: String) : Header } @@ -231,5 +226,5 @@ sealed interface ExtensionUiModel { data class Item( val extension: Extension, val installStep: InstallStep, - ) : ExtensionUiModel + ) }