Refactor how extensions list is modelled

To better enable changing the UI in the future based on sections.
This commit is contained in:
arkon 2023-01-10 23:18:34 -05:00
parent 6bb3070c57
commit 75b23c99ec
2 changed files with 85 additions and 94 deletions

View file

@ -146,79 +146,75 @@ private fun ExtensionContent(
} }
} }
items( state.items.forEach { (header, items) ->
items = state.items, item(
contentType = { contentType = "header",
when (it) { key = "extensionHeader-${header.hashCode()}",
is ExtensionUiModel.Header -> "header" ) {
is ExtensionUiModel.Item -> "item" when (header) {
} is ExtensionUiModel.Header.Resource -> {
}, val action: @Composable RowScope.() -> Unit =
key = { if (header.textRes == R.string.ext_updates_pending) {
when (it) { {
is ExtensionUiModel.Header -> "extensionHeader-${it.hashCode()}" Button(onClick = { onClickUpdateAll() }) {
is ExtensionUiModel.Item -> "extension-${it.hashCode()}" Text(
} text = stringResource(R.string.ext_update_all),
}, style = LocalTextStyle.current.copy(
) { item -> color = MaterialTheme.colorScheme.onPrimary,
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)
} }
} }
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) { if (trustState != null) {

View file

@ -40,7 +40,7 @@ class ExtensionsScreenModel(
init { init {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel) = { map -> val extensionMapper: (Map<String, InstallStep>) -> ((Extension) -> ExtensionUiModel.Item) = { map ->
{ {
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle) ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
} }
@ -80,38 +80,31 @@ class ExtensionsScreenModel(
) { query, downloads, (_updates, _installed, _available, _untrusted) -> ) { query, downloads, (_updates, _installed, _available, _untrusted) ->
val searchQuery = query ?: "" val searchQuery = query ?: ""
val languagesWithExtensions = _available val itemsGroups: ItemGroups = mutableMapOf()
.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<ExtensionUiModel>()
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
if (updates.isNotEmpty()) { if (updates.isNotEmpty()) {
items.add(ExtensionUiModel.Header.Resource(R.string.ext_updates_pending)) itemsGroups[ExtensionUiModel.Header.Resource(R.string.ext_updates_pending)] = updates
items.addAll(updates)
} }
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
if (installed.isNotEmpty() || untrusted.isNotEmpty()) { if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
items.add(ExtensionUiModel.Header.Resource(R.string.ext_installed)) itemsGroups[ExtensionUiModel.Header.Resource(R.string.ext_installed)] = installed + untrusted
items.addAll(installed)
items.addAll(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()) { if (languagesWithExtensions.isNotEmpty()) {
items.addAll(languagesWithExtensions) itemsGroups.putAll(languagesWithExtensions)
} }
items itemsGroups
} }
.collectLatest { .collectLatest {
mutableState.update { state -> mutableState.update { state ->
@ -140,10 +133,10 @@ class ExtensionsScreenModel(
coroutineScope.launchIO { coroutineScope.launchIO {
with(state.value) { with(state.value) {
if (isEmpty) return@launchIO if (isEmpty) return@launchIO
items items.values
.flatten()
.mapNotNull { .mapNotNull {
when { when {
it !is ExtensionUiModel.Item -> null
it.extension !is Extension.Installed -> null it.extension !is Extension.Installed -> null
!it.extension.hasUpdate -> null !it.extension.hasUpdate -> null
else -> it.extension else -> it.extension
@ -216,14 +209,16 @@ class ExtensionsScreenModel(
data class ExtensionsState( data class ExtensionsState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val items: List<ExtensionUiModel> = emptyList(), val items: ItemGroups = mutableMapOf(),
val updates: Int = 0, val updates: Int = 0,
) { ) {
val isEmpty = items.isEmpty() val isEmpty = items.isEmpty()
} }
sealed interface ExtensionUiModel { typealias ItemGroups = MutableMap<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
sealed interface Header : ExtensionUiModel {
object ExtensionUiModel {
sealed interface Header {
data class Resource(@StringRes val textRes: Int) : Header data class Resource(@StringRes val textRes: Int) : Header
data class Text(val text: String) : Header data class Text(val text: String) : Header
} }
@ -231,5 +226,5 @@ sealed interface ExtensionUiModel {
data class Item( data class Item(
val extension: Extension, val extension: Extension,
val installStep: InstallStep, val installStep: InstallStep,
) : ExtensionUiModel )
} }