Interactive formatting settings

This commit is contained in:
Sad Ellie 2023-05-21 22:20:43 +03:00
parent 9444e55d71
commit 7ec8cf934a
11 changed files with 568 additions and 128 deletions

View File

@ -22,28 +22,3 @@ package com.sadellie.unitto.core.base
* Current maximum scale that will be used in app. Used in various place in code * Current maximum scale that will be used in app. Used in various place in code
*/ */
const val MAX_PRECISION: Int = 1_000 const val MAX_PRECISION: Int = 1_000
/**
* Currently available scale options
*/
val PRECISIONS: Map<Int, Int> by lazy {
mapOf(
0 to R.string.precision_zero,
1 to R.string.precision_one,
2 to R.string.precision_two,
3 to R.string.precision_three,
4 to R.string.precision_four,
5 to R.string.precision_five,
6 to R.string.precision_six,
7 to R.string.precision_seven,
8 to R.string.precision_eight,
9 to R.string.precision_nine,
10 to R.string.precision_ten,
11 to R.string.precision_eleven,
12 to R.string.precision_twelve,
13 to R.string.precision_thirteen,
14 to R.string.precision_fourteen,
15 to R.string.precision_fifteen,
MAX_PRECISION to R.string.max_precision
)
}

View File

@ -1258,6 +1258,7 @@
<string name="third_party_licenses">Third party licenses</string> <string name="third_party_licenses">Third party licenses</string>
<string name="rate_this_app">Rate this app</string> <string name="rate_this_app">Rate this app</string>
<string name="formatting_settings_group">Formatting</string> <string name="formatting_settings_group">Formatting</string>
<string name="formatting_settings_support">Precision and numbers appearance</string>
<string name="additional_settings_group">Additional</string> <string name="additional_settings_group">Additional</string>
<!--Tools--> <!--Tools-->
@ -1306,9 +1307,9 @@
<!--Separator--> <!--Separator-->
<string name="separator_setting_support">Group separator symbol</string> <string name="separator_setting_support">Group separator symbol</string>
<string name="period">Period (42.069,12)</string> <string name="period">Period</string>
<string name="comma">Comma (42,069.12)</string> <string name="comma">Comma</string>
<string name="spaces">Spaces (42 069.12)</string> <string name="spaces">Spaces</string>
<!--Output format--> <!--Output format-->
<string name="output_format_setting_support">Result value formatting</string> <string name="output_format_setting_support">Result value formatting</string>

View File

@ -66,7 +66,7 @@ fun RowScope.SegmentedButton(
label: String, label: String,
onClick: () -> Unit, onClick: () -> Unit,
selected: Boolean, selected: Boolean,
icon: ImageVector icon: ImageVector? = null
) { ) {
val containerColor = val containerColor =
if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface
@ -81,14 +81,16 @@ fun RowScope.SegmentedButton(
), ),
contentPadding = PaddingValues(horizontal = 12.dp) contentPadding = PaddingValues(horizontal = 12.dp)
) { ) {
Crossfade(targetState = selected) { if (icon != null) {
if (it) { Crossfade(targetState = selected) {
Icon(Icons.Default.Check, null, Modifier.size(18.dp)) if (it) {
} else { Icon(Icons.Default.Check, null, Modifier.size(18.dp))
Icon(icon, null, Modifier.size(18.dp)) } else {
Icon(icon, null, Modifier.size(18.dp))
}
} }
Spacer(Modifier.width(8.dp))
} }
Spacer(Modifier.width(8.dp))
Text(label) Text(label)
} }
} }

View File

@ -0,0 +1,157 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.core.ui.common
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderPositions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ceil
import kotlin.math.roundToInt
@Composable
fun UnittoSlider(
modifier: Modifier = Modifier,
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (Float) -> Unit,
onValueChangeFinished: (Float) -> Unit = {}
) {
val animated = animateFloatAsState(targetValue = value)
Slider(
value = animated.value,
onValueChange = onValueChange,
modifier = modifier,
valueRange = valueRange,
onValueChangeFinished = { onValueChangeFinished(animated.value) },
track = { sliderPosition -> SquigglyTrack(sliderPosition) },
steps = valueRange.endInclusive.roundToInt(),
)
}
@Composable
private fun SquigglyTrack(
sliderPosition: SliderPositions,
eachWaveWidth: Float = 80f,
strokeWidth: Float = 15f,
filledColor: Color = MaterialTheme.colorScheme.primary,
unfilledColor: Color = MaterialTheme.colorScheme.surfaceVariant
) {
val coroutineScope = rememberCoroutineScope()
var direct by remember { mutableStateOf(1f) }
val animatedDirect = animateFloatAsState(direct, spring())
val slider = sliderPosition.activeRange.endInclusive
LaunchedEffect(sliderPosition.activeRange.endInclusive) {
coroutineScope.launch {
delay(300L)
direct = if (direct == 1f) -1f else 1f
}
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
) {
val width = size.width
val height = size.height
val path = Path().apply {
moveTo(
x = strokeWidth / 2,
y = height.times(0.5f)
)
val amount = ceil(width.div(eachWaveWidth))
repeat(amount.toInt()) {
val peek = if (it % 2 == 0) animatedDirect.value else -animatedDirect.value
relativeQuadraticBezierTo(
dx1 = eachWaveWidth * 0.5f,
// 0.75, because 1.0 was clipping out of bound for some reason
dy1 = height.times(0.75f) * peek,
dx2 = eachWaveWidth,
dy2 = 0f
)
}
}
clipRect(
top = 0f,
left = 0f,
right = width.times(slider),
bottom = height,
clipOp = ClipOp.Intersect
) {
drawPath(
path = path,
color = filledColor,
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
}
drawLine(
color = unfilledColor,
start = Offset(width.times(slider), height.times(0.5f)),
end = Offset(width, height.times(0.5f)),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
}
}
@Preview(device = "spec:width=411dp,height=891dp")
@Preview(device = "spec:width=673.5dp,height=841dp,dpi=480")
@Preview(device = "spec:width=1280dp,height=800dp,dpi=480")
@Preview(device = "spec:width=1920dp,height=1080dp,dpi=480")
@Composable
private fun PreviewNewSlider() {
var currentValue by remember { mutableStateOf(0.9f) }
UnittoSlider(
value = currentValue,
valueRange = 0f..1f,
onValueChange = { currentValue = it }
)
}

View File

@ -31,6 +31,7 @@ dependencies {
implementation(libs.com.github.sadellie.themmo) implementation(libs.com.github.sadellie.themmo)
implementation(libs.org.burnoutcrew.composereorderable) implementation(libs.org.burnoutcrew.composereorderable)
implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:model"))) implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:unitgroups"))) implementation(project(mapOf("path" to ":data:unitgroups")))
implementation(project(mapOf("path" to ":data:userprefs"))) implementation(project(mapOf("path" to ":data:userprefs")))

View File

@ -19,7 +19,6 @@
package com.sadellie.unitto.feature.settings package com.sadellie.unitto.feature.settings
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
@ -42,14 +41,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig import com.sadellie.unitto.core.base.BuildConfig
import com.sadellie.unitto.core.base.OUTPUT_FORMAT
import com.sadellie.unitto.core.base.PRECISIONS
import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.SEPARATORS
import com.sadellie.unitto.core.base.TOP_LEVEL_DESTINATIONS import com.sadellie.unitto.core.base.TOP_LEVEL_DESTINATIONS
import com.sadellie.unitto.core.ui.common.Header import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.MenuButton
@ -59,6 +54,7 @@ import com.sadellie.unitto.core.ui.openLink
import com.sadellie.unitto.data.model.UnitsListSorting import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.feature.settings.components.AlertDialogWithList import com.sadellie.unitto.feature.settings.components.AlertDialogWithList
import com.sadellie.unitto.feature.settings.navigation.aboutRoute import com.sadellie.unitto.feature.settings.navigation.aboutRoute
import com.sadellie.unitto.feature.settings.navigation.formattingRoute
import com.sadellie.unitto.feature.settings.navigation.themesRoute import com.sadellie.unitto.feature.settings.navigation.themesRoute
import com.sadellie.unitto.feature.settings.navigation.unitsGroupRoute import com.sadellie.unitto.feature.settings.navigation.unitsGroupRoute
@ -110,43 +106,18 @@ internal fun SettingsScreen(
) )
} }
// GENERAL GROUP // FORMATTING
item { Header(stringResource(R.string.formatting_settings_group)) }
// PRECISION
item { item {
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
Icons.Default._123, Icons.Default._123,
stringResource(R.string.precision_setting), stringResource(R.string.formatting_settings_group),
) )
}, },
headlineContent = { Text(stringResource(R.string.precision_setting)) }, headlineContent = { Text(stringResource(R.string.formatting_settings_group)) },
supportingContent = { Text(stringResource(R.string.precision_setting_support)) }, supportingContent = { Text(stringResource(R.string.formatting_settings_support)) },
modifier = Modifier.clickable { dialogState = DialogState.PRECISION } modifier = Modifier.clickable { navControllerAction(formattingRoute) }
)
}
// SEPARATOR
item {
ListItem(
headlineContent = { Text(stringResource(R.string.separator_setting)) },
supportingContent = { Text(stringResource(R.string.separator_setting_support)) },
modifier = Modifier
.clickable { dialogState = DialogState.SEPARATOR }
.padding(start = 40.dp)
)
}
// OUTPUT FORMAT
item {
ListItem(
headlineContent = { Text(stringResource(R.string.output_format_setting)) },
supportingContent = { Text(stringResource(R.string.output_format_setting_support)) },
modifier = Modifier
.clickable { dialogState = DialogState.OUTPUT_FORMAT }
.padding(start = 40.dp)
) )
} }
@ -260,35 +231,6 @@ internal fun SettingsScreen(
// Showing dialog // Showing dialog
when (dialogState) { when (dialogState) {
DialogState.PRECISION -> {
AlertDialogWithList(
title = stringResource(R.string.precision_setting),
listItems = PRECISIONS,
selectedItemIndex = userPrefs.value.digitsPrecision,
selectAction = viewModel::updatePrecision,
dismissAction = { resetDialog() },
supportText = stringResource(R.string.precision_setting_info)
)
}
DialogState.SEPARATOR -> {
AlertDialogWithList(
title = stringResource(R.string.separator_setting),
listItems = SEPARATORS,
selectedItemIndex = userPrefs.value.separator,
selectAction = viewModel::updateSeparator,
dismissAction = { resetDialog() }
)
}
DialogState.OUTPUT_FORMAT -> {
AlertDialogWithList(
title = stringResource(R.string.output_format_setting),
listItems = OUTPUT_FORMAT,
selectedItemIndex = userPrefs.value.outputFormat,
selectAction = viewModel::updateOutputFormat,
dismissAction = { resetDialog() },
supportText = stringResource(R.string.output_format_setting_info)
)
}
DialogState.START_SCREEN -> { DialogState.START_SCREEN -> {
AlertDialogWithList( AlertDialogWithList(
title = stringResource(R.string.starting_screen_setting), title = stringResource(R.string.starting_screen_setting),
@ -321,5 +263,5 @@ internal fun SettingsScreen(
* All possible states for alert dialog that opens when user clicks on settings. * All possible states for alert dialog that opens when user clicks on settings.
*/ */
private enum class DialogState { private enum class DialogState {
NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, START_SCREEN, UNIT_LIST_SORTING NONE, START_SCREEN, UNIT_LIST_SORTING
} }

View File

@ -40,33 +40,6 @@ class SettingsViewModel @Inject constructor(
UserPreferences() UserPreferences()
) )
/**
* @see UserPreferencesRepository.updateDigitsPrecision
*/
fun updatePrecision(precision: Int) {
viewModelScope.launch {
userPrefsRepository.updateDigitsPrecision(precision)
}
}
/**
* @see UserPreferencesRepository.updateSeparator
*/
fun updateSeparator(separator: Int) {
viewModelScope.launch {
userPrefsRepository.updateSeparator(separator)
}
}
/**
* @see UserPreferencesRepository.updateOutputFormat
*/
fun updateOutputFormat(outputFormat: Int) {
viewModelScope.launch {
userPrefsRepository.updateOutputFormat(outputFormat)
}
}
/** /**
* @see UserPreferencesRepository.updateVibrations * @see UserPreferencesRepository.updateVibrations
*/ */

View File

@ -0,0 +1,242 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.formatting
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Architecture
import androidx.compose.material.icons.filled.EMobiledata
import androidx.compose.material.icons.filled._123
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.OUTPUT_FORMAT
import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.SEPARATORS
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.UnittoSlider
import com.sadellie.unitto.core.ui.common.SegmentedButton
import com.sadellie.unitto.core.ui.common.SegmentedButtonsRow
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
import com.sadellie.unitto.core.ui.common.squashable
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
import kotlin.math.roundToInt
@Composable
fun FormattingRoute(
viewModel: FormattingViewModel = hiltViewModel(),
navigateUpAction: () -> Unit,
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
FormattingScreen(
navigateUpAction = navigateUpAction,
uiState = uiState.value,
onPrecisionChange = viewModel::updatePrecision,
onSeparatorChange = viewModel::updateSeparator,
onOutputFormatChange = viewModel::updateOutputFormat,
togglePreview = viewModel::togglePreview
)
}
@Composable
fun FormattingScreen(
navigateUpAction: () -> Unit,
uiState: FormattingUIState,
onPrecisionChange: (Int) -> Unit,
onSeparatorChange: (Int) -> Unit,
onOutputFormatChange: (Int) -> Unit,
togglePreview: () -> Unit,
precisions: ClosedFloatingPointRange<Float> = 0f..16f, // 16th is a MAX_PRECISION (1000)
) {
UnittoScreenWithLargeTopBar(
title = stringResource(R.string.formatting_settings_group),
navigationIcon = { NavigateUpButton(navigateUpAction) },
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
) {
item("preview") {
Column(
Modifier
.padding(16.dp)
.squashable(
onClick = togglePreview,
cornerRadiusRange = 8.dp..32.dp,
interactionSource = remember { MutableInteractionSource() }
)
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Preview (click to switch)",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
text = uiState.preview,
style = NumbersTextStyleDisplayMedium,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
item("precision_label") {
ListItem(
leadingContent = {
Icon(Icons.Default.Architecture, stringResource(R.string.precision_setting))
},
headlineContent = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.precision_setting))
Text(if (uiState.precision >= precisions.endInclusive) stringResource(R.string.max_precision) else uiState.precision.toString())
}
},
supportingContent = {
Text(stringResource(R.string.precision_setting_support))
}
)
}
item("precision_slider") {
UnittoSlider(
modifier = Modifier.padding(start = 56.dp, end = 16.dp),
value = uiState.precision.toFloat(),
valueRange = precisions,
onValueChange = { onPrecisionChange(it.roundToInt()) },
)
}
item("separator_label") {
ListItem(
leadingContent = {
Icon(Icons.Default._123, stringResource(R.string.precision_setting))
},
headlineContent = { Text(stringResource(R.string.separator_setting)) },
supportingContent = { Text(stringResource(R.string.separator_setting_support)) },
)
}
item("separator") {
Row(
Modifier
.horizontalScroll(rememberScrollState())
.wrapContentWidth()
.padding(start = 56.dp)
) {
SegmentedButtonsRow {
SEPARATORS.forEach { (separator, stringRes) ->
SegmentedButton(
label = stringResource(stringRes),
onClick = { onSeparatorChange(separator) },
selected = separator == uiState.separator
)
}
}
}
}
item("output_format_label") {
ListItem(
leadingContent = {
Icon(Icons.Default.EMobiledata, stringResource(R.string.precision_setting))
},
headlineContent = { Text(stringResource(R.string.output_format_setting)) },
supportingContent = { Text(stringResource(R.string.output_format_setting_support)) }
)
}
item("output_format") {
Row(
Modifier
.horizontalScroll(rememberScrollState())
.wrapContentWidth()
.padding(start = 56.dp)
) {
SegmentedButtonsRow {
OUTPUT_FORMAT.forEach { (outputFormat, stringRes) ->
SegmentedButton(
label = stringResource(stringRes),
onClick = { onOutputFormatChange(outputFormat) },
selected = outputFormat == uiState.outputFormat
)
}
}
}
}
}
}
}
@Preview
@Composable
private fun PreviewFormattingScreen() {
var currentPrecision by remember { mutableStateOf(6) }
var currentSeparator by remember { mutableStateOf(Separator.COMMA) }
var currentOutputFormat by remember { mutableStateOf(OutputFormat.PLAIN) }
FormattingScreen(
uiState = FormattingUIState(
preview = "",
precision = 3,
separator = Separator.SPACES,
outputFormat = OutputFormat.PLAIN
),
onPrecisionChange = { currentPrecision = it },
onSeparatorChange = { currentSeparator = it },
onOutputFormatChange = { currentOutputFormat = it },
navigateUpAction = {},
togglePreview = {}
)
}

View File

@ -0,0 +1,26 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.formatting
data class FormattingUIState(
val preview: String = "",
val precision: Int = 0,
val separator: Int? = null,
val outputFormat: Int? = null
)

View File

@ -0,0 +1,113 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.formatting
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.core.base.MAX_PRECISION
import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.formatExpression
import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.toStringWith
import com.sadellie.unitto.data.common.trimZeros
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.math.ceil
@HiltViewModel
class FormattingViewModel @Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
private val _mainPreferences = userPreferencesRepository.mainPreferencesFlow
private val _fractional = MutableStateFlow(false)
val uiState = combine(_mainPreferences, _fractional) { mainPrefs, fractional ->
return@combine FormattingUIState(
preview = updatePreview(
fractional = fractional,
precision = mainPrefs.digitsPrecision,
outputFormat = mainPrefs.outputFormat,
separator = mainPrefs.separator
),
precision = mainPrefs.digitsPrecision,
separator = mainPrefs.separator,
outputFormat = mainPrefs.outputFormat
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), FormattingUIState())
fun togglePreview() = _fractional.update { !it }
private fun updatePreview(
fractional: Boolean,
precision: Int,
outputFormat: Int,
separator: Int,
): String {
val bigD = when {
fractional -> "0.${"0000001".padStart(precision, '0')}"
precision > 0 -> "123456.${"7890123456".repeat(ceil(precision.toDouble() / 10.0).toInt())}"
else -> "123456"
}
return BigDecimal(bigD)
.setMinimumRequiredScale(precision)
.trimZeros()
.toStringWith(outputFormat)
.formatExpression(AllFormatterSymbols.getById(separator))
}
/**
* @see UserPreferencesRepository.updateDigitsPrecision
*/
fun updatePrecision(precision: Int) {
viewModelScope.launch {
// In UI the slider for precision goes from 0 to 16, where 16 is treated as 1000 (MAX)
val newPrecision = if (precision > 15) MAX_PRECISION else precision
userPreferencesRepository.updateDigitsPrecision(newPrecision)
}
}
/**
* @see UserPreferencesRepository.updateSeparator
*/
fun updateSeparator(separator: Int) {
viewModelScope.launch {
userPreferencesRepository.updateSeparator(separator)
}
}
/**
* @see UserPreferencesRepository.updateOutputFormat
*/
fun updateOutputFormat(outputFormat: Int) {
viewModelScope.launch {
userPreferencesRepository.updateOutputFormat(outputFormat)
}
}
}

View File

@ -27,8 +27,9 @@ import androidx.navigation.compose.navigation
import com.sadellie.unitto.core.base.TopLevelDestinations import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.feature.settings.AboutScreen import com.sadellie.unitto.feature.settings.AboutScreen
import com.sadellie.unitto.feature.settings.SettingsScreen import com.sadellie.unitto.feature.settings.SettingsScreen
import com.sadellie.unitto.feature.settings.themes.ThemesRoute
import com.sadellie.unitto.feature.settings.ThirdPartyLicensesScreen import com.sadellie.unitto.feature.settings.ThirdPartyLicensesScreen
import com.sadellie.unitto.feature.settings.formatting.FormattingRoute
import com.sadellie.unitto.feature.settings.themes.ThemesRoute
import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsScreen import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsScreen
import io.github.sadellie.themmo.ThemmoController import io.github.sadellie.themmo.ThemmoController
@ -38,6 +39,7 @@ internal const val themesRoute = "themes_route"
internal const val unitsGroupRoute = "units_group_route" internal const val unitsGroupRoute = "units_group_route"
internal const val thirdPartyRoute = "third_party_route" internal const val thirdPartyRoute = "third_party_route"
internal const val aboutRoute = "about_route" internal const val aboutRoute = "about_route"
internal const val formattingRoute = "formatting_route"
fun NavController.navigateToSettings(builder: NavOptionsBuilder.() -> Unit) { fun NavController.navigateToSettings(builder: NavOptionsBuilder.() -> Unit) {
navigate(settingsRoute, builder) navigate(settingsRoute, builder)
@ -85,5 +87,11 @@ fun NavGraphBuilder.settingGraph(
navigateUpAction = navController::navigateUp, navigateUpAction = navController::navigateUp,
) )
} }
composable(formattingRoute) {
FormattingRoute(
navigateUpAction = navController::navigateUp
)
}
} }
} }