Personal UnitGroups list

User can now reorder/add/hide unit groups.
Lots of stuff were implemented in a very strange/ugly way, but will be fixed in later commits.
This commit is contained in:
Sad Ellie 2022-08-12 20:31:39 +03:00
parent 0f25e83c6e
commit e3c1c5376f
17 changed files with 370 additions and 30 deletions

View File

@ -194,4 +194,7 @@ dependencies {
// Themmo // Themmo
implementation("com.github.sadellie:themmo:0.0.3") implementation("com.github.sadellie:themmo:0.0.3")
// ComposeReorderable
implementation("org.burnoutcrew.composereorderable:reorderable:0.9.2")
} }

View File

@ -40,15 +40,17 @@ import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN
import com.sadellie.unitto.data.NavRoutes.SETTINGS_GRAPH import com.sadellie.unitto.data.NavRoutes.SETTINGS_GRAPH
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN
import com.sadellie.unitto.data.NavRoutes.UNIT_GROUPS_SCREEN
import com.sadellie.unitto.screens.MainViewModel import com.sadellie.unitto.screens.MainViewModel
import com.sadellie.unitto.screens.setttings.SettingsViewModel
import com.sadellie.unitto.screens.setttings.AboutScreen
import com.sadellie.unitto.screens.main.MainScreen import com.sadellie.unitto.screens.main.MainScreen
import com.sadellie.unitto.screens.second.LeftSideScreen import com.sadellie.unitto.screens.second.LeftSideScreen
import com.sadellie.unitto.screens.second.RightSideScreen import com.sadellie.unitto.screens.second.RightSideScreen
import com.sadellie.unitto.screens.second.SecondViewModel import com.sadellie.unitto.screens.second.SecondViewModel
import com.sadellie.unitto.screens.setttings.AboutScreen
import com.sadellie.unitto.screens.setttings.SettingsScreen import com.sadellie.unitto.screens.setttings.SettingsScreen
import com.sadellie.unitto.screens.setttings.SettingsViewModel
import com.sadellie.unitto.screens.setttings.ThemesScreen import com.sadellie.unitto.screens.setttings.ThemesScreen
import com.sadellie.unitto.screens.setttings.UnitGroupsScreen
import com.sadellie.unitto.ui.theme.AppTypography import com.sadellie.unitto.ui.theme.AppTypography
import com.sadellie.unitto.ui.theme.DarkThemeColors import com.sadellie.unitto.ui.theme.DarkThemeColors
import com.sadellie.unitto.ui.theme.LightThemeColors import com.sadellie.unitto.ui.theme.LightThemeColors
@ -168,5 +170,12 @@ private fun NavGraphBuilder.settingGraph(
composable(ABOUT_SCREEN) { composable(ABOUT_SCREEN) {
AboutScreen(navigateUpAction = { navController.navigateUp() }) AboutScreen(navigateUpAction = { navController.navigateUp() })
} }
composable(UNIT_GROUPS_SCREEN) {
UnitGroupsScreen(
viewModel = settingsViewModel,
navigateUpAction = { navController.navigateUp() }
)
}
} }
} }

View File

@ -31,4 +31,5 @@ object NavRoutes {
const val SETTINGS_SCREEN = "SettingsScreen" const val SETTINGS_SCREEN = "SettingsScreen"
const val THEMES_SCREEN = "ThemesScreen" const val THEMES_SCREEN = "ThemesScreen"
const val ABOUT_SCREEN = "AboutScreen" const val ABOUT_SCREEN = "AboutScreen"
const val UNIT_GROUPS_SCREEN = "UnitGroupScreen"
} }

View File

@ -18,6 +18,8 @@
package com.sadellie.unitto.data.preferences package com.sadellie.unitto.data.preferences
import android.text.TextUtils.split
import android.util.Log
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
@ -25,8 +27,10 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import com.sadellie.unitto.data.units.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.MyUnitIDS import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.UnitGroup
import io.github.sadellie.themmo.ThemingMode import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
@ -47,6 +51,7 @@ import javax.inject.Inject
* @property latestLeftSideUnit Latest [AbstractUnit] that was on the left side * @property latestLeftSideUnit Latest [AbstractUnit] that was on the left side
* @property latestRightSideUnit Latest [AbstractUnit] that was on the right side * @property latestRightSideUnit Latest [AbstractUnit] that was on the right side
* @property enableAnalytics Whether or not user wants to share application usage data * @property enableAnalytics Whether or not user wants to share application usage data
* @property shownUnitGroups [UnitGroup]s that user wants to see. Excludes other [UnitGroup]s
*/ */
data class UserPreferences( data class UserPreferences(
val themingMode: ThemingMode? = null, val themingMode: ThemingMode? = null,
@ -57,7 +62,8 @@ data class UserPreferences(
val outputFormat: Int = OutputFormat.PLAIN, val outputFormat: Int = OutputFormat.PLAIN,
val latestLeftSideUnit: String = MyUnitIDS.kilometer, val latestLeftSideUnit: String = MyUnitIDS.kilometer,
val latestRightSideUnit: String = MyUnitIDS.mile, val latestRightSideUnit: String = MyUnitIDS.mile,
val enableAnalytics: Boolean = true val enableAnalytics: Boolean = true,
val shownUnitGroups: List<UnitGroup> = ALL_UNIT_GROUPS
) )
/** /**
@ -77,6 +83,7 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
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 ENABLE_ANALYTICS = booleanPreferencesKey("ENABLE_ANALYTICS_PREF_KEY") val ENABLE_ANALYTICS = booleanPreferencesKey("ENABLE_ANALYTICS_PREF_KEY")
val SHOWN_UNIT_GROUPS = stringPreferencesKey("SHOWN_UNIT_GROUPS_PREF_KEY")
} }
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
@ -107,6 +114,19 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
preferences[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile preferences[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
val enableAnalytics: Boolean = val enableAnalytics: Boolean =
preferences[PrefsKeys.ENABLE_ANALYTICS] ?: true preferences[PrefsKeys.ENABLE_ANALYTICS] ?: true
val shownUnitGroups: List<UnitGroup> =
preferences[PrefsKeys.SHOWN_UNIT_GROUPS]?.let { list ->
// Everything is in hidden (nothing in shown)
list.ifEmpty { return@let listOf() }
try {
list.split(",").map { UnitGroup.valueOf(it) }
} catch (e: Exception) {
// Bad thing happened, return null so all units will be shown
null
}
} ?: ALL_UNIT_GROUPS
UserPreferences( UserPreferences(
themingMode = themingMode, themingMode = themingMode,
@ -117,7 +137,8 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
outputFormat = outputFormat, outputFormat = outputFormat,
latestLeftSideUnit = latestLeftSideUnit, latestLeftSideUnit = latestLeftSideUnit,
latestRightSideUnit = latestRightSideUnit, latestRightSideUnit = latestRightSideUnit,
enableAnalytics = enableAnalytics enableAnalytics = enableAnalytics,
shownUnitGroups = shownUnitGroups
) )
} }
@ -211,4 +232,10 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
preferences[PrefsKeys.ENABLE_AMOLED_THEME] = enabled preferences[PrefsKeys.ENABLE_AMOLED_THEME] = enabled
} }
} }
suspend fun updateShownUnitGroups(shownUnitGroups: List<UnitGroup>) {
dataStore.edit { preferences ->
preferences[PrefsKeys.SHOWN_UNIT_GROUPS] = shownUnitGroups.joinToString(",")
}
}
} }

View File

@ -96,16 +96,21 @@ class AllUnitsRepository @Inject constructor() {
* @param favoritesOnly When True will filter only [AbstractUnit]s with [AbstractUnit.isFavorite] * @param favoritesOnly When True will filter only [AbstractUnit]s with [AbstractUnit.isFavorite]
* set to True. * set to True.
* @param searchQuery When not empty, will search by [AbstractUnit.renderedName] using [sortByLev]. * @param searchQuery When not empty, will search by [AbstractUnit.renderedName] using [sortByLev].
* @param allUnitsGroups All [UnitGroup]s. Determines which units will be used for filtering.
* @return Grouped by [UnitGroup] list of [AbstractUnit]s. * @return Grouped by [UnitGroup] list of [AbstractUnit]s.
*/ */
fun filterUnits( fun filterUnits(
hideBrokenCurrencies: Boolean, hideBrokenCurrencies: Boolean,
chosenUnitGroup: UnitGroup?, chosenUnitGroup: UnitGroup?,
favoritesOnly: Boolean, favoritesOnly: Boolean,
searchQuery: String searchQuery: String,
allUnitsGroups: List<UnitGroup>
): Map<UnitGroup, List<AbstractUnit>> { ): Map<UnitGroup, List<AbstractUnit>> {
val shownUnits: List<AbstractUnit> =
allUnitsGroups.flatMap { getCollectionByGroup(it) ?: listOf() }
var basicFilteredUnits: Sequence<AbstractUnit> = var basicFilteredUnits: Sequence<AbstractUnit> =
getCollectionByGroup(unitGroup = chosenUnitGroup)?.asSequence() ?: allUnits.asSequence() getCollectionByGroup(unitGroup = chosenUnitGroup)?.asSequence() ?: shownUnits.asSequence()
if (favoritesOnly) { if (favoritesOnly) {
basicFilteredUnits = basicFilteredUnits.filter { it.isFavorite } basicFilteredUnits = basicFilteredUnits.filter { it.isFavorite }

View File

@ -0,0 +1,106 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022 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.data.units
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 {
add(
shownUnitGroups.value.indexOfFirst { it == to.key },
removeAt(shownUnitGroups.value.indexOfFirst { it == from.key })
)
}
}
}
}

View File

@ -151,6 +151,7 @@ fun LeftSideScreen(
viewModel = viewModel, viewModel = viewModel,
chipsRow = { unitGroup, lazyListState -> chipsRow = { unitGroup, lazyListState ->
ChipsRow( ChipsRow(
items = viewModel.uiState.shownUnitGroups,
chosenUnitGroup = unitGroup, chosenUnitGroup = unitGroup,
selectAction = { selectAction = {
viewModel.toggleSelectedChip(it) viewModel.toggleSelectedChip(it)

View File

@ -27,11 +27,13 @@ import com.sadellie.unitto.data.units.UnitGroup
* @property favoritesOnly Whether or not show only favorite [AbstractUnit]s. * @property favoritesOnly Whether or not show only favorite [AbstractUnit]s.
* @property unitsToShow Grouped list of [AbstractUnit]s. * @property unitsToShow Grouped list of [AbstractUnit]s.
* @property searchQuery Search query in search bar. * @property searchQuery Search query in search bar.
* @property shownUnitGroups All [UnitGroup]s that can be seen in chips row
* @property chosenUnitGroup Currently selected chip. Nul means that no chip is selected. * @property chosenUnitGroup Currently selected chip. Nul means that no chip is selected.
*/ */
data class SecondScreenUIState( data class SecondScreenUIState(
val favoritesOnly: Boolean = false, val favoritesOnly: Boolean = false,
val unitsToShow: Map<UnitGroup, List<AbstractUnit>> = emptyMap(), val unitsToShow: Map<UnitGroup, List<AbstractUnit>> = emptyMap(),
val searchQuery: String = "", val searchQuery: String = "",
val shownUnitGroups: List<UnitGroup> = listOf(),
val chosenUnitGroup: UnitGroup? = null val chosenUnitGroup: UnitGroup? = null
) )

View File

@ -26,6 +26,7 @@ import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.AllUnitsRepository import com.sadellie.unitto.data.units.AllUnitsRepository
import com.sadellie.unitto.data.units.UnitGroup import com.sadellie.unitto.data.units.UnitGroup
import com.sadellie.unitto.data.units.UnitGroupsRepository
import com.sadellie.unitto.data.units.database.MyBasedUnit import com.sadellie.unitto.data.units.database.MyBasedUnit
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -37,7 +38,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SecondViewModel @Inject constructor( class SecondViewModel @Inject constructor(
private val basedUnitRepository: MyBasedUnitsRepository, private val basedUnitRepository: MyBasedUnitsRepository,
private val allUnitsRepository: AllUnitsRepository private val allUnitsRepository: AllUnitsRepository,
private val unitGroupsRepository: UnitGroupsRepository
) : ViewModel() { ) : ViewModel() {
var uiState: SecondScreenUIState by mutableStateOf(SecondScreenUIState()) var uiState: SecondScreenUIState by mutableStateOf(SecondScreenUIState())
@ -93,7 +95,8 @@ class SecondViewModel @Inject constructor(
hideBrokenCurrencies = hideBrokenCurrencies, hideBrokenCurrencies = hideBrokenCurrencies,
chosenUnitGroup = uiState.chosenUnitGroup, chosenUnitGroup = uiState.chosenUnitGroup,
favoritesOnly = uiState.favoritesOnly, favoritesOnly = uiState.favoritesOnly,
searchQuery = uiState.searchQuery searchQuery = uiState.searchQuery,
allUnitsGroups = uiState.shownUnitGroups
) )
uiState = uiState.copy(unitsToShow = unitsToShow) uiState = uiState.copy(unitsToShow = unitsToShow)
@ -119,4 +122,12 @@ class SecondViewModel @Inject constructor(
) )
} }
} }
init {
viewModelScope.launch {
unitGroupsRepository.shownUnitGroups.collect {
uiState = uiState.copy(shownUnitGroups = it)
}
}
}
} }

View File

@ -31,11 +31,12 @@ import androidx.compose.ui.unit.dp
* Unit group header. * Unit group header.
* *
* @param text Unit group name. * @param text Unit group name.
* @param modifier Modifier that will be applied to Text composable.
*/ */
@Composable @Composable
fun Header(text: String) { fun Header(text: String, modifier: Modifier = Modifier) {
Text( Text(
modifier = Modifier modifier = modifier
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 8.dp), .padding(vertical = 12.dp, horizontal = 8.dp),

View File

@ -30,6 +30,7 @@ import com.sadellie.unitto.BuildConfig
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN
import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN
import com.sadellie.unitto.data.NavRoutes.UNIT_GROUPS_SCREEN
import com.sadellie.unitto.data.preferences.OUTPUT_FORMAT import com.sadellie.unitto.data.preferences.OUTPUT_FORMAT
import com.sadellie.unitto.data.preferences.PRECISIONS import com.sadellie.unitto.data.preferences.PRECISIONS
import com.sadellie.unitto.data.preferences.SEPARATORS import com.sadellie.unitto.data.preferences.SEPARATORS
@ -59,42 +60,49 @@ fun SettingsScreen(
// GENERAL GROUP // GENERAL GROUP
item { SettingsHeader(stringResource(R.string.general_settings_group)) } item { SettingsHeader(stringResource(R.string.general_settings_group)) }
// THEME
item {
SettingsListItem(
label = stringResource(id = R.string.unit_groups_setting)
) { navControllerAction(UNIT_GROUPS_SCREEN) }
}
// PRECISION // PRECISION
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.precision_setting), label = stringResource(R.string.precision_setting),
stringResource(R.string.precision_setting_support) supportText = stringResource(R.string.precision_setting_support)
) { dialogState = DialogState.PRECISION } ) { dialogState = DialogState.PRECISION }
} }
// SEPARATOR // SEPARATOR
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.separator_setting), label = stringResource(R.string.separator_setting),
stringResource(R.string.separator_setting_support) supportText = stringResource(R.string.separator_setting_support)
) { dialogState = DialogState.SEPARATOR } ) { dialogState = DialogState.SEPARATOR }
} }
// OUTPUT FORMAT // OUTPUT FORMAT
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.output_format_setting), label = stringResource(R.string.output_format_setting),
stringResource(R.string.output_format_setting_support) supportText = stringResource(R.string.output_format_setting_support)
) { dialogState = DialogState.OUTPUT_FORMAT } ) { dialogState = DialogState.OUTPUT_FORMAT }
} }
// THEME // THEME
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.theme_setting), label = stringResource(R.string.theme_setting),
stringResource(R.string.theme_setting_support) supportText = stringResource(R.string.theme_setting_support)
) { navControllerAction(THEMES_SCREEN) } ) { navControllerAction(THEMES_SCREEN) }
} }
// CURRENCY RATE NOTE // CURRENCY RATE NOTE
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.currency_rates_note_setting) label = stringResource(R.string.currency_rates_note_setting)
) { dialogState = DialogState.CURRENCY_RATE } ) { dialogState = DialogState.CURRENCY_RATE }
} }
@ -104,14 +112,14 @@ fun SettingsScreen(
// TERMS AND CONDITIONS // TERMS AND CONDITIONS
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.terms_and_conditions) label = stringResource(R.string.terms_and_conditions)
) { openLink(mContext, "http://sadellie.github.io/unitto/terms-app.html") } ) { openLink(mContext, "http://sadellie.github.io/unitto/terms-app.html") }
} }
// PRIVACY POLICY // PRIVACY POLICY
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.privacy_policy) label = stringResource(R.string.privacy_policy)
) { openLink(mContext, "http://sadellie.github.io/unitto/privacy-app.html") } ) { openLink(mContext, "http://sadellie.github.io/unitto/privacy-app.html") }
} }
@ -119,9 +127,9 @@ fun SettingsScreen(
if (BuildConfig.ANALYTICS) { if (BuildConfig.ANALYTICS) {
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.send_usage_statistics), label = stringResource(R.string.send_usage_statistics),
stringResource(R.string.send_usage_statistics_support), supportText = stringResource(R.string.send_usage_statistics_support),
viewModel.userPrefs.enableAnalytics switchState = viewModel.userPrefs.enableAnalytics
) { viewModel.updateEnableAnalytics(it) } ) { viewModel.updateEnableAnalytics(it) }
} }
} }
@ -129,7 +137,7 @@ fun SettingsScreen(
// THIRD PARTY // THIRD PARTY
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.third_party_licenses) label = stringResource(R.string.third_party_licenses)
) { navControllerAction(ABOUT_SCREEN) } ) { navControllerAction(ABOUT_SCREEN) }
} }
@ -137,7 +145,7 @@ fun SettingsScreen(
if (BuildConfig.STORE_LINK.isNotEmpty()) { if (BuildConfig.STORE_LINK.isNotEmpty()) {
item { item {
SettingsListItem( SettingsListItem(
stringResource(R.string.rate_this_app) label = stringResource(R.string.rate_this_app)
) { openLink(mContext, BuildConfig.STORE_LINK) } ) { openLink(mContext, BuildConfig.STORE_LINK) }
} }
} }

View File

@ -27,18 +27,25 @@ import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.FirebaseHelper import com.sadellie.unitto.FirebaseHelper
import com.sadellie.unitto.data.preferences.UserPreferences import com.sadellie.unitto.data.preferences.UserPreferences
import com.sadellie.unitto.data.preferences.UserPreferencesRepository import com.sadellie.unitto.data.preferences.UserPreferencesRepository
import com.sadellie.unitto.data.units.UnitGroup
import com.sadellie.unitto.data.units.UnitGroupsRepository
import com.sadellie.unitto.screens.Formatter import com.sadellie.unitto.screens.Formatter
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.themmo.ThemingMode import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository, private val userPrefsRepository: UserPreferencesRepository,
private val unitGroupsRepository: UnitGroupsRepository,
private val application: Application, private val application: Application,
) : ViewModel() { ) : ViewModel() {
var userPrefs: UserPreferences by mutableStateOf(UserPreferences()) var userPrefs: UserPreferences by mutableStateOf(UserPreferences())
val shownUnitGroups = unitGroupsRepository.shownUnitGroups
val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
/** /**
* @see [UserPreferencesRepository.updateThemingMode] * @see [UserPreferencesRepository.updateThemingMode]
@ -104,8 +111,53 @@ class SettingsViewModel @Inject constructor(
} }
} }
/**
* See [UnitGroupsRepository.markUnitGroupAsHidden] and
* [UserPreferencesRepository.updateShownUnitGroups]
*/
fun hideUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsHidden(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* See [UnitGroupsRepository.markUnitGroupAsShown] and
* [UserPreferencesRepository.updateShownUnitGroups]
*/
fun returnUnitGroup(unitGroup: UnitGroup) {
viewModelScope.launch {
unitGroupsRepository.markUnitGroupAsShown(unitGroup)
userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
}
}
/**
* See [UnitGroupsRepository.moveShownUnitGroups] and
* [UserPreferencesRepository.updateShownUnitGroups]
*/
fun onMove(from: ItemPosition, to: ItemPosition) {
viewModelScope.launch {
unitGroupsRepository.moveShownUnitGroups(from, to)
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 { init {
viewModelScope.launch { viewModelScope.launch {
unitGroupsRepository.updateShownGroups(
userPrefsRepository.userPreferencesFlow.first().shownUnitGroups
)
userPrefsRepository.userPreferencesFlow.collect { userPrefsRepository.userPreferencesFlow.collect {
userPrefs = it userPrefs = it
Formatter.setSeparator(it.separator) Formatter.setSeparator(it.separator)

View File

@ -0,0 +1,107 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022 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.screens.setttings
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.R
import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar
import com.sadellie.unitto.screens.second.components.Header
import com.sadellie.unitto.screens.setttings.components.SettingsListItem
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
@Composable
fun UnitGroupsScreen(
viewModel: SettingsViewModel,
navigateUpAction: () -> Unit
) {
UnittoLargeTopAppBar(
title = stringResource(R.string.unit_groups_setting),
navigateUpAction = navigateUpAction
) { paddingValues ->
val shownUnits = viewModel.shownUnitGroups.collectAsState()
val hiddenUnits = viewModel.hiddenUnitGroups.collectAsState()
val state = rememberReorderableLazyListState(
onMove = viewModel::onMove,
canDragOver = viewModel::canDragOver
)
LazyColumn(
state = state.listState,
modifier = Modifier
.padding(paddingValues)
.reorderable(state)
.detectReorderAfterLongPress(state)
) {
item(key = "enabled") {
Header(text = "Enabled")
}
items(shownUnits.value, { it }) { item ->
ReorderableItem(state, key = item) { isDragging ->
val background = animateColorAsState(
if (isDragging) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface
)
val cornerRadius = animateDpAsState(if (isDragging) 16.dp else 0.dp)
SettingsListItem(
modifier = Modifier
.padding(horizontal = cornerRadius.value)
.clip(RoundedCornerShape(cornerRadius.value))
.background(background.value),
label = stringResource(item.res),
onClick = { viewModel.hideUnitGroup(item) }
)
}
}
item(key = "disabled") {
Header(
text = "Disabled",
modifier = Modifier.animateItemPlacement()
)
}
items(hiddenUnits.value, { it }) {
SettingsListItem(
modifier = Modifier.animateItemPlacement(),
label = stringResource(it.res),
onClick = { viewModel.returnUnitGroup(it) }
)
}
}
}
}

View File

@ -59,6 +59,7 @@ import com.sadellie.unitto.R
* This component can be easily modified if you provide additional component to it, * This component can be easily modified if you provide additional component to it,
* for example a switch or a checkbox. * for example a switch or a checkbox.
* *
* @param modifier Modifier that will be applied to a Row.
* @param label Main text. * @param label Main text.
* @param supportText Text that is located below label. * @param supportText Text that is located below label.
* @param onClick Action to perform when user clicks on this component (whole component is clickable). * @param onClick Action to perform when user clicks on this component (whole component is clickable).
@ -66,13 +67,14 @@ import com.sadellie.unitto.R
*/ */
@Composable @Composable
private fun BasicSettingsListItem( private fun BasicSettingsListItem(
modifier: Modifier = Modifier,
label: String, label: String,
supportText: String? = null, supportText: String? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
content: @Composable RowScope.() -> Unit = {} content: @Composable RowScope.() -> Unit = {}
) { ) {
Row( Row(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
@ -111,16 +113,18 @@ private fun BasicSettingsListItem(
/** /**
* Represents one item in list on Settings screen. * Represents one item in list on Settings screen.
* *
* @param modifier Modifier that will be applied to a Row.
* @param label Main text. * @param label Main text.
* @param supportText Text that is located below label. * @param supportText Text that is located below label.
* @param onClick Action to perform when user clicks on this component (whole component is clickable). * @param onClick Action to perform when user clicks on this component (whole component is clickable).
*/ */
@Composable @Composable
fun SettingsListItem( fun SettingsListItem(
modifier: Modifier = Modifier,
label: String, label: String,
supportText: String? = null, supportText: String? = null,
onClick: () -> Unit, onClick: () -> Unit,
) = BasicSettingsListItem(label, supportText, onClick) ) = BasicSettingsListItem(modifier, label, supportText, onClick)
/** /**
* Represents one item in list on Settings screen. * Represents one item in list on Settings screen.
@ -137,7 +141,7 @@ fun SettingsListItem(
supportText: String? = null, supportText: String? = null,
switchState: Boolean, switchState: Boolean,
onSwitchChange: (Boolean) -> Unit onSwitchChange: (Boolean) -> Unit
) = BasicSettingsListItem(label, supportText, { onSwitchChange(!switchState) }) { ) = BasicSettingsListItem(Modifier, label, supportText, { onSwitchChange(!switchState) }) {
Switch(checked = switchState, onCheckedChange = { onSwitchChange(it) }) Switch(checked = switchState, onCheckedChange = { onSwitchChange(it) })
} }
@ -158,7 +162,7 @@ fun <T> SettingsListItem(
allOptions: Map<T, String>, allOptions: Map<T, String>,
selected: T, selected: T,
onSelectedChange: (T) -> Unit onSelectedChange: (T) -> Unit
) = BasicSettingsListItem(label, supportText, {}) { ) = BasicSettingsListItem(Modifier, label, supportText, {}) {
var dropDownExpanded by rememberSaveable { mutableStateOf(false) } var dropDownExpanded by rememberSaveable { mutableStateOf(false) }
var currentOption by rememberSaveable { mutableStateOf(selected) } var currentOption by rememberSaveable { mutableStateOf(selected) }
val dropDownRotation: Float by animateFloatAsState( val dropDownRotation: Float by animateFloatAsState(

View File

@ -741,5 +741,6 @@
<string name="color_theme">Colour theme</string> <string name="color_theme">Colour theme</string>
<string name="drop_down_description">Open or close drop down menu</string> <string name="drop_down_description">Open or close drop down menu</string>
<string name="hello_label">Hello!</string> <string name="hello_label">Hello!</string>
<string name="unit_groups_setting">Unit groups</string>
</resources> </resources>

View File

@ -676,5 +676,6 @@
<string name="color_theme">Цветовая тема</string> <string name="color_theme">Цветовая тема</string>
<string name="drop_down_description">Открыть или закрыть меню</string> <string name="drop_down_description">Открыть или закрыть меню</string>
<string name="hello_label">Привет!</string> <string name="hello_label">Привет!</string>
<string name="unit_groups_setting">Группы величин</string>
</resources> </resources>

View File

@ -940,6 +940,7 @@
<string name="precision_setting">Precision</string> <string name="precision_setting">Precision</string>
<string name="separator_setting">Separator</string> <string name="separator_setting">Separator</string>
<string name="output_format_setting">Output format</string> <string name="output_format_setting">Output format</string>
<string name="unit_groups_setting">Unit groups</string>
<string name="currency_rates_note_setting">Wrong currency rates?</string> <string name="currency_rates_note_setting">Wrong currency rates?</string>
<string name="currency_rates_note_title">Note</string> <string name="currency_rates_note_title">Note</string>
<string name="currency_rates_note_text">Currency rates are updated daily. There\'s no real-time market monitoring in the app</string> <string name="currency_rates_note_text">Currency rates are updated daily. There\'s no real-time market monitoring in the app</string>