Refactor UnitGroupsScreen

This commit is contained in:
Sad Ellie 2024-02-06 18:07:25 +03:00
parent 5c81a1c675
commit 32eb7422d5
13 changed files with 219 additions and 238 deletions

View File

@ -21,17 +21,10 @@ package com.sadellie.unitto.data.model
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.base.R
val ALL_UNIT_GROUPS: List<UnitGroup> by lazy {
UnitGroup.entries
}
/**
* As not all measurements can be converted between each other, we separate them into groups.
* Within one group all measurements can be converted
*/
enum class UnitGroup( enum class UnitGroup(
@StringRes val res: Int @StringRes val res: Int
) { ) {
// NOTE: This order is used as default for new users
LENGTH(res = R.string.unit_group_length), LENGTH(res = R.string.unit_group_length),
CURRENCY(res = R.string.unit_group_currency), CURRENCY(res = R.string.unit_group_currency),
MASS(res = R.string.unit_group_mass), MASS(res = R.string.unit_group_mass),

View File

@ -71,6 +71,10 @@ interface UserPreferencesRepository {
suspend fun updateShownUnitGroups(shownUnitGroups: List<UnitGroup>) suspend fun updateShownUnitGroups(shownUnitGroups: List<UnitGroup>)
suspend fun addShownUnitGroup(unitGroup: UnitGroup)
suspend fun removeShownUnitGroup(unitGroup: UnitGroup)
suspend fun updateLastReadChangelog(value: String) suspend fun updateLastReadChangelog(value: String)
suspend fun updateVibrations(enabled: Boolean) suspend fun updateVibrations(enabled: Boolean)

View File

@ -23,90 +23,105 @@ import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.Separator import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.base.TopLevelDestinations import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.data.converter.UnitID import com.sadellie.unitto.data.converter.UnitID
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.UnitsListSorting
import io.github.sadellie.themmo.core.MonetMode import io.github.sadellie.themmo.core.MonetMode
import io.github.sadellie.themmo.core.ThemingMode import io.github.sadellie.themmo.core.ThemingMode
fun Preferences.getEnableDynamicTheme(): Boolean { internal fun Preferences.getEnableDynamicTheme(): Boolean {
return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true return this[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: true
} }
fun Preferences.getThemingMode(): ThemingMode { internal fun Preferences.getThemingMode(): ThemingMode {
return this[PrefsKeys.THEMING_MODE] return this[PrefsKeys.THEMING_MODE]
?.letTryOrNull { ThemingMode.valueOf(it) } ?.letTryOrNull { ThemingMode.valueOf(it) }
?: ThemingMode.AUTO ?: ThemingMode.AUTO
} }
fun Preferences.getEnableAmoledTheme(): Boolean { internal fun Preferences.getEnableAmoledTheme(): Boolean {
return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false return this[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
} }
fun Preferences.getCustomColor(): Long { internal fun Preferences.getCustomColor(): Long {
return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified return this[PrefsKeys.CUSTOM_COLOR] ?: 16L // From Color.Unspecified
} }
fun Preferences.getMonetMode(): MonetMode { internal fun Preferences.getMonetMode(): MonetMode {
return this[PrefsKeys.MONET_MODE] return this[PrefsKeys.MONET_MODE]
?.letTryOrNull { MonetMode.valueOf(it) } ?.letTryOrNull { MonetMode.valueOf(it) }
?: MonetMode.TonalSpot ?: MonetMode.TonalSpot
} }
fun Preferences.getStartingScreen(): String { internal fun Preferences.getStartingScreen(): String {
return this[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.CALCULATOR_GRAPH return this[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.CALCULATOR_GRAPH
} }
fun Preferences.getEnableToolsExperiment(): Boolean { internal fun Preferences.getEnableToolsExperiment(): Boolean {
return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false return this[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
} }
fun Preferences.getSystemFont(): Boolean { internal fun Preferences.getSystemFont(): Boolean {
return this[PrefsKeys.SYSTEM_FONT] ?: false return this[PrefsKeys.SYSTEM_FONT] ?: false
} }
fun Preferences.getLastReadChangelog(): String { internal fun Preferences.getLastReadChangelog(): String {
return this[PrefsKeys.LAST_READ_CHANGELOG] ?: "" return this[PrefsKeys.LAST_READ_CHANGELOG] ?: ""
} }
fun Preferences.getEnableVibrations(): Boolean { internal fun Preferences.getEnableVibrations(): Boolean {
return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true return this[PrefsKeys.ENABLE_VIBRATIONS] ?: true
} }
fun Preferences.getRadianMode(): Boolean { internal fun Preferences.getRadianMode(): Boolean {
return this[PrefsKeys.RADIAN_MODE] ?: true return this[PrefsKeys.RADIAN_MODE] ?: true
} }
fun Preferences.getSeparator(): Int { internal fun Preferences.getSeparator(): Int {
return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE return this[PrefsKeys.SEPARATOR] ?: Separator.SPACE
} }
fun Preferences.getMiddleZero(): Boolean { internal fun Preferences.getMiddleZero(): Boolean {
return this[PrefsKeys.MIDDLE_ZERO] ?: true return this[PrefsKeys.MIDDLE_ZERO] ?: true
} }
fun Preferences.getPartialHistoryView(): Boolean { internal fun Preferences.getPartialHistoryView(): Boolean {
return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true return this[PrefsKeys.PARTIAL_HISTORY_VIEW] ?: true
} }
fun Preferences.getDigitsPrecision(): Int { internal fun Preferences.getDigitsPrecision(): Int {
return this[PrefsKeys.DIGITS_PRECISION] ?: 3 return this[PrefsKeys.DIGITS_PRECISION] ?: 3
} }
fun Preferences.getOutputFormat(): Int { internal fun Preferences.getOutputFormat(): Int {
return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN return this[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
} }
fun Preferences.getUnitConverterFormatTime(): Boolean { internal fun Preferences.getUnitConverterFormatTime(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false return this[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
} }
fun Preferences.getUnitConverterSorting(): UnitsListSorting { internal fun Preferences.getUnitConverterSorting(): UnitsListSorting {
return this[PrefsKeys.UNIT_CONVERTER_SORTING] return this[PrefsKeys.UNIT_CONVERTER_SORTING]
?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE ?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
} }
fun Preferences.getShownUnitGroups(): List<UnitGroup> { // TODO Remove when 80% users are on sets
internal fun Preferences.getShownUnitGroups(): List<UnitGroup> {
// Uncomment once legacy is gone
// return this[PrefsKeys.ENABLED_UNIT_GROUPS]
// ?.letTryOrNull { stringSet: Set<String> ->
// stringSet.map { UnitGroup.valueOf(it) }
// } ?: UnitGroup.entries
// Try new method
val unitGroups: List<UnitGroup>? = this[PrefsKeys.ENABLED_UNIT_GROUPS]
?.letTryOrNull { stringSet: Set<String> ->
stringSet.map { UnitGroup.valueOf(it) }
}
if (!unitGroups.isNullOrEmpty()) return unitGroups
// Failed to get from sets, try old method
return this[PrefsKeys.SHOWN_UNIT_GROUPS] return this[PrefsKeys.SHOWN_UNIT_GROUPS]
?.letTryOrNull { list -> ?.letTryOrNull { list ->
list list
@ -114,23 +129,23 @@ fun Preferences.getShownUnitGroups(): List<UnitGroup> {
.split(",") .split(",")
.map { UnitGroup.valueOf(it) } .map { UnitGroup.valueOf(it) }
} }
?: ALL_UNIT_GROUPS ?: UnitGroup.entries
} }
fun Preferences.getUnitConverterFavoritesOnly(): Boolean { internal fun Preferences.getUnitConverterFavoritesOnly(): Boolean {
return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] return this[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY]
?: false ?: false
} }
fun Preferences.getLatestLeftSide(): String { internal fun Preferences.getLatestLeftSide(): String {
return this[PrefsKeys.LATEST_LEFT_SIDE] ?: UnitID.kilometer return this[PrefsKeys.LATEST_LEFT_SIDE] ?: UnitID.kilometer
} }
fun Preferences.getLatestRightSide(): String { internal fun Preferences.getLatestRightSide(): String {
return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: UnitID.mile return this[PrefsKeys.LATEST_RIGHT_SIDE] ?: UnitID.mile
} }
fun Preferences.getAcButton(): Boolean { internal fun Preferences.getAcButton(): Boolean {
return this[PrefsKeys.AC_BUTTON] ?: true return this[PrefsKeys.AC_BUTTON] ?: true
} }

View File

@ -18,7 +18,6 @@
package com.sadellie.unitto.data.userprefs package com.sadellie.unitto.data.userprefs
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.userprefs.AboutPreferences import com.sadellie.unitto.data.model.userprefs.AboutPreferences
@ -90,7 +89,7 @@ data class FormattingPreferencesImpl(
) : FormattingPreferences ) : FormattingPreferences
data class UnitGroupsPreferencesImpl( data class UnitGroupsPreferencesImpl(
override val shownUnitGroups: List<UnitGroup> = ALL_UNIT_GROUPS, override val shownUnitGroups: List<UnitGroup> = UnitGroup.entries,
) : UnitGroupsPreferences ) : UnitGroupsPreferences
data class AddSubtractPreferencesImpl( data class AddSubtractPreferencesImpl(

View File

@ -22,6 +22,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
object PrefsKeys { object PrefsKeys {
// COMMON // COMMON
@ -52,6 +53,7 @@ object PrefsKeys {
val LATEST_LEFT_SIDE = stringPreferencesKey("LATEST_LEFT_SIDE_PREF_KEY") val LATEST_LEFT_SIDE = stringPreferencesKey("LATEST_LEFT_SIDE_PREF_KEY")
val LATEST_RIGHT_SIDE = stringPreferencesKey("LATEST_RIGHT_SIDE_PREF_KEY") val LATEST_RIGHT_SIDE = stringPreferencesKey("LATEST_RIGHT_SIDE_PREF_KEY")
val SHOWN_UNIT_GROUPS = stringPreferencesKey("SHOWN_UNIT_GROUPS_PREF_KEY") val SHOWN_UNIT_GROUPS = stringPreferencesKey("SHOWN_UNIT_GROUPS_PREF_KEY")
val ENABLED_UNIT_GROUPS = stringSetPreferencesKey("ENABLED_UNIT_GROUPS_PREF_KEY")
val UNIT_CONVERTER_FAVORITES_ONLY = booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY") val UNIT_CONVERTER_FAVORITES_ONLY = booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY")
val UNIT_CONVERTER_FORMAT_TIME = booleanPreferencesKey("UNIT_CONVERTER_FORMAT_TIME_PREF_KEY") val UNIT_CONVERTER_FORMAT_TIME = booleanPreferencesKey("UNIT_CONVERTER_FORMAT_TIME_PREF_KEY")
val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY") val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY")

View File

@ -221,7 +221,25 @@ class UserPreferencesRepositoryImpl @Inject constructor(
override suspend fun updateShownUnitGroups(shownUnitGroups: List<UnitGroup>) { override suspend fun updateShownUnitGroups(shownUnitGroups: List<UnitGroup>) {
dataStore.edit { preferences -> dataStore.edit { preferences ->
preferences[PrefsKeys.SHOWN_UNIT_GROUPS] = shownUnitGroups.joinToString(",") preferences[PrefsKeys.ENABLED_UNIT_GROUPS] = shownUnitGroups
.map { it.name }
.toSet()
}
}
override suspend fun addShownUnitGroup(unitGroup: UnitGroup) {
dataStore.edit { preferences ->
preferences[PrefsKeys.ENABLED_UNIT_GROUPS] = preferences[PrefsKeys.ENABLED_UNIT_GROUPS]
?.plus(unitGroup.name)
?: emptySet()
}
}
override suspend fun removeShownUnitGroup(unitGroup: UnitGroup) {
dataStore.edit { preferences ->
preferences[PrefsKeys.ENABLED_UNIT_GROUPS] = preferences[PrefsKeys.ENABLED_UNIT_GROUPS]
?.minus(unitGroup.name)
?: emptySet()
} }
} }

View File

@ -41,7 +41,6 @@ import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.AssistChip import com.sadellie.unitto.core.ui.common.AssistChip
import com.sadellie.unitto.core.ui.common.FilterChip import com.sadellie.unitto.core.ui.common.FilterChip
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitGroup
/** /**
@ -55,7 +54,7 @@ import com.sadellie.unitto.data.model.UnitGroup
*/ */
@Composable @Composable
internal fun ChipsRow( internal fun ChipsRow(
items: List<UnitGroup> = ALL_UNIT_GROUPS, items: List<UnitGroup> = UnitGroup.entries,
chosenUnitGroup: UnitGroup?, chosenUnitGroup: UnitGroup?,
selectAction: (UnitGroup?) -> Unit, selectAction: (UnitGroup?) -> Unit,
navigateToSettingsAction: () -> Unit, navigateToSettingsAction: () -> Unit,
@ -105,7 +104,7 @@ fun PreviewUnittoChips() {
} }
ChipsRow( ChipsRow(
items = ALL_UNIT_GROUPS, items = UnitGroup.entries,
chosenUnitGroup = selected, chosenUnitGroup = selected,
selectAction = { selectAction(it) }, selectAction = { selectAction(it) },
navigateToSettingsAction = {}, navigateToSettingsAction = {},

View File

@ -41,7 +41,7 @@ import com.sadellie.unitto.core.ui.common.EmptyScreen
import com.sadellie.unitto.core.ui.common.ListItem import com.sadellie.unitto.core.ui.common.ListItem
import com.sadellie.unitto.core.ui.common.NavigateUpButton import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.ScaffoldWithLargeTopBar import com.sadellie.unitto.core.ui.common.ScaffoldWithLargeTopBar
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.userprefs.ConverterPreferences import com.sadellie.unitto.data.model.userprefs.ConverterPreferences
import com.sadellie.unitto.data.userprefs.ConverterPreferencesImpl import com.sadellie.unitto.data.userprefs.ConverterPreferencesImpl
@ -139,7 +139,7 @@ private fun PreviewConverterSettingsScreen() {
outputFormat = OutputFormat.PLAIN, outputFormat = OutputFormat.PLAIN,
unitConverterFormatTime = false, unitConverterFormatTime = false,
unitConverterSorting = UnitsListSorting.USAGE, unitConverterSorting = UnitsListSorting.USAGE,
shownUnitGroups = ALL_UNIT_GROUPS, shownUnitGroups = UnitGroup.entries,
unitConverterFavoritesOnly = false, unitConverterFavoritesOnly = false,
enableToolsExperiment = false, enableToolsExperiment = false,
latestLeftSideUnit = "kilometer", latestLeftSideUnit = "kilometer",

View File

@ -34,7 +34,7 @@ import com.sadellie.unitto.feature.settings.formatting.FormattingRoute
import com.sadellie.unitto.feature.settings.language.LanguageRoute import com.sadellie.unitto.feature.settings.language.LanguageRoute
import com.sadellie.unitto.feature.settings.startingscreen.StartingScreenRoute import com.sadellie.unitto.feature.settings.startingscreen.StartingScreenRoute
import com.sadellie.unitto.feature.settings.thirdparty.ThirdPartyLicensesScreen import com.sadellie.unitto.feature.settings.thirdparty.ThirdPartyLicensesScreen
import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsScreen import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsRoute
import io.github.sadellie.themmo.ThemmoController import io.github.sadellie.themmo.ThemmoController
private val graph = DrawerItem.Settings.graph private val graph = DrawerItem.Settings.graph
@ -115,7 +115,7 @@ fun NavGraphBuilder.settingGraph(
} }
unittoStackedComposable(unitsGroupRoute) { unittoStackedComposable(unitsGroupRoute) {
UnitGroupsScreen( UnitGroupsRoute(
navigateUpAction = navController::navigateUp, navigateUpAction = navController::navigateUp,
) )
} }

View File

@ -1,115 +0,0 @@
/*
* Unitto is a calculator for Android
* Copyright (c) 2023-2024 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.settings.unitgroups
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository that holds information about shown and hidden [UnitGroup]s and provides methods to
* show/hide [UnitGroup]s.
*/
@Singleton
class UnitGroupsRepository @Inject constructor() {
/**
* Mutex is need needed because we work with flow (sync stuff).
*/
private val mutex = Mutex()
/**
* Currently shown [UnitGroup]s.
*/
var shownUnitGroups = MutableStateFlow(listOf<UnitGroup>())
private set
/**
* Currently hidden [UnitGroup]s.
*/
var hiddenUnitGroups = MutableStateFlow(listOf<UnitGroup>())
private set
/**
* Sets [shownUnitGroups] and updates [hiddenUnitGroups] as a side effect. [hiddenUnitGroups] is
* everything from [ALL_UNIT_GROUPS] that was not in [shownUnitGroups].
*
* @param list List of [UnitGroup]s that need to be shown.
*/
suspend fun updateShownGroups(list: List<UnitGroup>) {
mutex.withLock {
shownUnitGroups.value = list
hiddenUnitGroups.value = ALL_UNIT_GROUPS - list.toSet()
}
}
/**
* Moves [UnitGroup] from [shownUnitGroups] to [hiddenUnitGroups]
*
* @param unitGroup [UnitGroup] to hide.
*/
suspend fun markUnitGroupAsHidden(unitGroup: UnitGroup) {
mutex.withLock {
shownUnitGroups.value = shownUnitGroups.value - unitGroup
// Newly hidden unit will appear at the top of the list
hiddenUnitGroups.value = listOf(unitGroup) + hiddenUnitGroups.value
}
}
/**
* Moves [UnitGroup] from [hiddenUnitGroups] to [shownUnitGroups]
*
* @param unitGroup [UnitGroup] to show.
*/
suspend fun markUnitGroupAsShown(unitGroup: UnitGroup) {
mutex.withLock {
hiddenUnitGroups.value = hiddenUnitGroups.value - unitGroup
shownUnitGroups.value = shownUnitGroups.value + unitGroup
}
}
/**
* Moves [UnitGroup] in [shownUnitGroups] from one index to another (reorder).
*
* @param from Position from which we need to move from
* @param to Position where to put [UnitGroup]
*/
suspend fun moveShownUnitGroups(from: ItemPosition, to: ItemPosition) {
mutex.withLock {
shownUnitGroups.value = shownUnitGroups.value.toMutableList().apply {
val initialIndex = shownUnitGroups.value.indexOfFirst { it == from.key }
/**
* No such item. Happens when dragging item and clicking "remove" while item is
* still being dragged.
*/
if (initialIndex == -1) return
add(
shownUnitGroups.value.indexOfFirst { it == to.key },
removeAt(initialIndex)
)
}
}
}
}

View File

@ -40,18 +40,23 @@ import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.EmptyScreen
import com.sadellie.unitto.core.ui.common.Header import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.NavigateUpButton import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.ScaffoldWithLargeTopBar import com.sadellie.unitto.core.ui.common.ScaffoldWithLargeTopBar
import com.sadellie.unitto.data.model.UnitGroup
import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorder import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.detectReorderAfterLongPress
@ -59,22 +64,52 @@ import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable import org.burnoutcrew.reorderable.reorderable
@Composable @Composable
internal fun UnitGroupsScreen( internal fun UnitGroupsRoute(
viewModel: UnitGroupsViewModel = hiltViewModel(), viewModel: UnitGroupsViewModel = hiltViewModel(),
navigateUpAction: () -> Unit, navigateUpAction: () -> Unit,
) {
when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
UnitGroupsUIState.Loading -> EmptyScreen()
is UnitGroupsUIState.Ready -> UnitGroupsScreen(
uiState = uiState,
navigateUpAction = navigateUpAction,
updateShownUnitGroups = viewModel::updateShownUnitGroups,
addShownUnitGroup = viewModel::addShownUnitGroup,
removeShownUnitGroup = viewModel::removeShownUnitGroup,
)
}
}
@Composable
private fun UnitGroupsScreen(
uiState: UnitGroupsUIState.Ready,
navigateUpAction: () -> Unit,
updateShownUnitGroups: (List<UnitGroup>) -> Unit,
addShownUnitGroup: (UnitGroup) -> Unit,
removeShownUnitGroup: (UnitGroup) -> Unit,
) { ) {
ScaffoldWithLargeTopBar( ScaffoldWithLargeTopBar(
title = stringResource(R.string.settings_unit_groups_title), title = stringResource(R.string.settings_unit_groups_title),
navigationIcon = { NavigateUpButton(navigateUpAction) } navigationIcon = { NavigateUpButton(navigateUpAction) }
) { paddingValues -> ) { paddingValues ->
val copiedShownList = rememberUpdatedState(uiState.shownUnitGroups) as MutableState
val shownUnits = viewModel.shownUnitGroups.collectAsState()
val hiddenUnits = viewModel.hiddenUnitGroups.collectAsState()
val state = rememberReorderableLazyListState( val state = rememberReorderableLazyListState(
onMove = viewModel::onMove, onMove = { from, to ->
canDragOver = { from, _ -> viewModel.canDragOver(from) }, copiedShownList.value = copiedShownList.value
onDragEnd = { _, _ -> viewModel.onDragEnd() } .toMutableList()
.apply {
// -1 for list header
add(to.index - 1, removeAt(from.index - 1))
}
},
canDragOver = { draggedOver, _ ->
// offset by 1 for list header
draggedOver.index in 1..(copiedShownList.value.lastIndex + 1)
},
onDragEnd = onDragEnd@{ from, to ->
if (from == to) return@onDragEnd
updateShownUnitGroups(copiedShownList.value)
}
) )
LazyColumn( LazyColumn(
@ -90,12 +125,18 @@ internal fun UnitGroupsScreen(
) )
} }
items(shownUnits.value, { it }) { item -> items(copiedShownList.value, { it }) { item ->
ReorderableItem(state, key = item) { isDragging -> ReorderableItem(state, key = item) { isDragging ->
val transition = updateTransition(isDragging, label = "draggedTransition") val transition = updateTransition(isDragging, label = "draggedTransition")
val background by transition.animateColor(label = "background") { val background by transition.animateColor(label = "background") {
if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface
} }
val textColor by transition.animateColor(label = "background") {
if (it) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
}
val iconColor by transition.animateColor(label = "background") {
if (it) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.outline
}
val itemPadding by transition.animateDp(label = "itemPadding") { val itemPadding by transition.animateDp(label = "itemPadding") {
if (it) 16.dp else 0.dp if (it) 16.dp else 0.dp
} }
@ -105,28 +146,29 @@ internal fun UnitGroupsScreen(
modifier = Modifier modifier = Modifier
.padding(horizontal = itemPadding) .padding(horizontal = itemPadding)
.clip(CircleShape) .clip(CircleShape)
.clickable { viewModel.hideUnitGroup(item) } .clickable { removeShownUnitGroup(item) }
.detectReorderAfterLongPress(state), .detectReorderAfterLongPress(state),
colors = ListItemDefaults.colors( colors = ListItemDefaults.colors(
containerColor = background containerColor = background,
headlineColor = textColor,
leadingIconColor = iconColor,
trailingIconColor = iconColor,
), ),
leadingContent = { leadingContent = {
Icon( Icon(
Icons.Default.RemoveCircle, imageVector = Icons.Default.RemoveCircle,
stringResource(R.string.settings_disable_unit_group_description), contentDescription = stringResource(R.string.settings_disable_unit_group_description),
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.clickable( modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(false), indication = rememberRipple(false),
onClick = { viewModel.hideUnitGroup(item) } onClick = { removeShownUnitGroup(item) }
) )
) )
}, },
trailingContent = { trailingContent = {
Icon( Icon(
Icons.Default.DragHandle, imageVector = Icons.Default.DragHandle,
stringResource(R.string.settings_reorder_unit_group_description), contentDescription = stringResource(R.string.settings_reorder_unit_group_description),
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
@ -148,11 +190,11 @@ internal fun UnitGroupsScreen(
) )
} }
items(hiddenUnits.value, { it }) { items(uiState.hiddenUnitGroups, { it }) {
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.clickable { viewModel.returnUnitGroup(it) } .clickable { addShownUnitGroup(it) }
.animateItemPlacement(), .animateItemPlacement(),
headlineContent = { Text(stringResource(it.res)) }, headlineContent = { Text(stringResource(it.res)) },
trailingContent = { trailingContent = {
@ -163,7 +205,7 @@ internal fun UnitGroupsScreen(
modifier = Modifier.clickable( modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(false), indication = rememberRipple(false),
onClick = { viewModel.returnUnitGroup(it) } onClick = { addShownUnitGroup(it) }
) )
) )
} }
@ -172,3 +214,20 @@ internal fun UnitGroupsScreen(
} }
} }
} }
@Preview
@Composable
private fun PreviewUnitGroupsScreen() {
val shownUnitGroups = UnitGroup.entries.take(4)
UnitGroupsScreen(
uiState = UnitGroupsUIState.Ready(
shownUnitGroups = shownUnitGroups,
hiddenUnitGroups = UnitGroup.entries - shownUnitGroups.toSet()
),
navigateUpAction = {},
updateShownUnitGroups = {},
addShownUnitGroup = {},
removeShownUnitGroup = {},
)
}

View File

@ -0,0 +1,30 @@
/*
* Unitto is a calculator for Android
* Copyright (c) 2024 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.settings.unitgroups
import com.sadellie.unitto.data.model.UnitGroup
internal sealed class UnitGroupsUIState {
data object Loading: UnitGroupsUIState()
data class Ready(
val shownUnitGroups: List<UnitGroup>,
val hiddenUnitGroups: List<UnitGroup>
): UnitGroupsUIState()
}

View File

@ -20,75 +20,52 @@ package com.sadellie.unitto.feature.settings.unitgroups
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class UnitGroupsViewModel @Inject constructor( internal class UnitGroupsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository, private val userPrefsRepository: UserPreferencesRepository,
private val unitGroupsRepository: UnitGroupsRepository,
) : ViewModel() { ) : ViewModel() {
val shownUnitGroups = unitGroupsRepository.shownUnitGroups
val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
/** val uiState = userPrefsRepository.unitGroupsPrefs
* @see UnitGroupsRepository.markUnitGroupAsHidden .map {
* @see UserPreferencesRepository.updateShownUnitGroups UnitGroupsUIState.Ready(
*/ shownUnitGroups = it.shownUnitGroups,
fun hideUnitGroup(unitGroup: UnitGroup) { hiddenUnitGroups = UnitGroup.entries - it.shownUnitGroups.toSet()
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsHidden(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* @see UnitGroupsRepository.markUnitGroupAsShown
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun returnUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsShown(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* @see UnitGroupsRepository.moveShownUnitGroups
*/
fun onMove(from: ItemPosition, to: ItemPosition) {
viewModelScope.launch {
unitGroupsRepository.moveShownUnitGroups(from, to)
}
}
/**
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun onDragEnd() {
viewModelScope.launch {
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* Prevent from dragging over non-draggable items (headers and hidden)
*
* @param pos Position we are dragging over.
* @return True if can drag over given item.
*/
fun canDragOver(pos: ItemPosition) = shownUnitGroups.value.any { it == pos.key }
init {
viewModelScope.launch {
unitGroupsRepository.updateShownGroups(
userPrefsRepository.unitGroupsPrefs.first().shownUnitGroups
) )
} }
.stateIn(viewModelScope, UnitGroupsUIState.Loading)
/**
* @see UserPreferencesRepository.removeShownUnitGroup
*/
fun removeShownUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
userPrefsRepository.removeShownUnitGroup(unitGroup)
}
}
/**
* @see UserPreferencesRepository.addShownUnitGroup
*/
fun addShownUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
userPrefsRepository.addShownUnitGroup(unitGroup)
}
}
/**
* @see UserPreferencesRepository.updateShownUnitGroups
*/
fun updateShownUnitGroups(unitGroups: List<UnitGroup>) {
viewModelScope.launch {
userPrefsRepository.updateShownUnitGroups(unitGroups)
}
} }
} }