mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Interactive formatting settings
This commit is contained in:
parent
9444e55d71
commit
7ec8cf934a
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
|
@ -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,6 +81,7 @@ fun RowScope.SegmentedButton(
|
|||||||
),
|
),
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp)
|
contentPadding = PaddingValues(horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
|
if (icon != null) {
|
||||||
Crossfade(targetState = selected) {
|
Crossfade(targetState = selected) {
|
||||||
if (it) {
|
if (it) {
|
||||||
Icon(Icons.Default.Check, null, Modifier.size(18.dp))
|
Icon(Icons.Default.Check, null, Modifier.size(18.dp))
|
||||||
@ -89,6 +90,7 @@ fun RowScope.SegmentedButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
Text(label)
|
Text(label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
@ -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")))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user