Merge branch 'refactor/ui'

This commit is contained in:
Sad Ellie 2022-12-30 20:53:14 +04:00
commit 84b428e620
16 changed files with 524 additions and 416 deletions

View File

@ -0,0 +1,74 @@
/*
* 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
import com.sadellie.unitto.data.preferences.OutputFormat
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.floor
import kotlin.math.log10
/**
* Shorthand function to use correct `toString` method according to [outputFormat].
*/
fun BigDecimal.toStringWith(outputFormat: Int): String {
// Setting result value using a specified OutputFormat
return when (outputFormat) {
OutputFormat.ALLOW_ENGINEERING -> this.toString()
OutputFormat.FORCE_ENGINEERING -> this.toEngineeringString()
else -> this.toPlainString()
}
}
/**
* Sets the minimum scale that is required to get first non zero value in fractional part
*
* @param[prefScale] Is the preferred scale, the one which will be compared against
*/
fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
/**
* Here we are getting the amount of zeros in fractional part before non zero value
* For example, for 0.00000123456 we need the length of 00000
* Next we add one to get the position of the first non zero value
* Also, this block is only for VERY small numbers
*/
return this.setScale(
kotlin.math.max(
prefScale,
if (this.abs() < BigDecimal.ONE) {
// https://stackoverflow.com/a/46136593
-floor(log10(this.abs().remainder(BigDecimal.ONE).toDouble())).toInt()
} else {
0
}
),
RoundingMode.HALF_EVEN
)
}
/**
* Removes all trailing zeroes.
*
* @throws NumberFormatException if value is bigger than [Double.MAX_VALUE] to avoid memory overflow.
*/
fun BigDecimal.trimZeros(): BigDecimal {
if (this.abs() > BigDecimal.valueOf(Double.MAX_VALUE)) throw NumberFormatException()
return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros()
}

View File

@ -0,0 +1,42 @@
/*
* 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
import kotlinx.coroutines.flow.Flow
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> =
kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
)
}

View File

@ -19,6 +19,7 @@
package com.sadellie.unitto.data.units package com.sadellie.unitto.data.units
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.sadellie.unitto.screens.lev
import java.math.BigDecimal import java.math.BigDecimal
/** /**
@ -64,3 +65,64 @@ abstract class AbstractUnit(
scale: Int scale: Int
): BigDecimal ): BigDecimal
} }
/**
* Sorts sequence of units by Levenshtein distance
*
* @param stringA String for Levenshtein distance
* @return Sorted sequence of units. Units with lower Levenshtein distance are higher
*/
fun Sequence<AbstractUnit>.sortByLev(stringA: String): Sequence<AbstractUnit> {
val stringToCompare = stringA.lowercase()
// We don't need units where name is too different, half of the symbols is wrong in this situation
val threshold: Int = stringToCompare.length / 2
// List of pair: Unit and it's levDist
val unitsWithDist = mutableListOf<Pair<AbstractUnit, Int>>()
this.forEach { unit ->
val unitName: String = unit.renderedName.lowercase()
/**
* There is chance that unit name doesn't need any edits (contains part of the query)
* So computing levDist is a waste of resources
*/
when {
// It's the best possible match if it start with
unitName.startsWith(stringToCompare) -> {
unitsWithDist.add(Pair(unit, 0))
return@forEach
}
// It's a little bit worse when it just contains part of the query
unitName.contains(stringToCompare) -> {
unitsWithDist.add(Pair(unit, 1))
return@forEach
}
}
/**
* Levenshtein Distance for this specific name of this unit
*
* We use substring so that we compare not the whole unit name, but only part of it.
* It's required because without it levDist will be too high for units with longer
* names than the search query
*
* For example:
* Search query is 'Kelometer' and unit name is 'Kilometer per hour'
* Without substring levDist will be 9 which means that this unit will be skipped
*
* With substring levDist will be 3 so unit will be included
*/
val levDist = unitName
.substring(0, minOf(stringToCompare.length, unitName.length))
.lev(stringToCompare)
// Threshold
if (levDist < threshold) {
unitsWithDist.add(Pair(unit, levDist))
}
}
// Sorting by levDist and getting units
return unitsWithDist
.sortedBy { it.second }
.map { it.first }
.asSequence()
}

View File

@ -21,10 +21,9 @@ package com.sadellie.unitto.data.units
import android.content.Context import android.content.Context
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.data.preferences.MAX_PRECISION import com.sadellie.unitto.data.preferences.MAX_PRECISION
import com.sadellie.unitto.data.setMinimumRequiredScale
import com.sadellie.unitto.data.trimZeros
import com.sadellie.unitto.data.units.database.MyBasedUnit import com.sadellie.unitto.data.units.database.MyBasedUnit
import com.sadellie.unitto.screens.setMinimumRequiredScale
import com.sadellie.unitto.screens.sortByLev
import com.sadellie.unitto.screens.trimZeros
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import javax.inject.Inject import javax.inject.Inject

View File

@ -20,8 +20,8 @@ package com.sadellie.unitto.data.units
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.sadellie.unitto.data.preferences.MAX_PRECISION import com.sadellie.unitto.data.preferences.MAX_PRECISION
import com.sadellie.unitto.screens.setMinimumRequiredScale import com.sadellie.unitto.data.setMinimumRequiredScale
import com.sadellie.unitto.screens.trimZeros import com.sadellie.unitto.data.trimZeros
import java.math.BigDecimal import java.math.BigDecimal
/** /**

View File

@ -0,0 +1,116 @@
/*
* 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
import com.sadellie.unitto.data.KEY_COMMA
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_E
import com.sadellie.unitto.data.KEY_LEFT_BRACKET
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
import com.sadellie.unitto.data.OPERATORS
import com.sadellie.unitto.data.preferences.Separator
object Formatter {
private const val SPACE = " "
private const val PERIOD = "."
private const val COMMA = ","
/**
* Grouping separator.
*/
private var grouping: String = SPACE
/**
* Fractional part separator.
*/
var fractional = KEY_COMMA
/**
* Change current separator to another [separator].
*
* @see [Separator]
*/
fun setSeparator(separator: Int) {
grouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
}
/**
* Format [input].
*
* This will replace operators to their more appealing variants: divide, multiply and minus.
* Plus operator remains unchanged.
*
* Numbers will also be formatted.
*
* @see [formatNumber]
*/
fun format(input: String): String {
// Don't do anything to engineering string.
if (input.contains(KEY_E)) return input.replace(KEY_DOT, fractional)
var output = input
// We may receive expressions
// Find all numbers in that expression
val allNumbers: List<String> = input.split(
*OPERATORS.toTypedArray(), KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET
)
allNumbers.forEach {
output = output.replace(it, formatNumber(it))
}
return output
}
/**
* Format given [input].
*
* Input must be a number. Will replace grouping separators and fractional part separators.
*
* @see grouping
* @see fractional
*/
private fun formatNumber(input: String): String {
val splitInput = input.split(".")
var firstPart = splitInput[0]
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
val offset = 3 - firstPart.length.mod(3)
var output = if (offset != 3) {
// We add some spaces at the begging so that last chunk has 3 symbols
firstPart = " ".repeat(offset) + firstPart
firstPart.chunked(3).joinToString(this.grouping).drop(offset)
} else {
firstPart.chunked(3).joinToString(this.grouping)
}
// Handling fractional part
if (input.contains(".")) {
output = output + fractional + splitInput.getOrElse(1) { "" }
}
return output
}
}

View File

@ -0,0 +1,60 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-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
/**
* Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
* first.
*
* @return The amount of changes that are needed to transform one string into another
*/
fun String.lev(secondString: String): Int {
val stringA = this.lowercase()
val stringB = secondString.lowercase()
// Skipping computation for this cases
if (stringA == stringB) return 0
if (stringA.isEmpty()) return stringB.length
// This case is basically unreal in this app, because secondString is a unit name and they are never empty
if (stringB.isEmpty()) return stringA.length
var cost = IntArray(stringA.length + 1) { it }
var newCost = IntArray(stringA.length + 1)
for (i in 1..stringB.length) {
// basically shifting this to the right by 1 each time
newCost[0] = i
for (j in 1..stringA.length) {
newCost[j] = minOf(
// Adding 1 if they don't match, i.e. need to replace
cost[j - 1] + if (stringA[j - 1] == stringB[i - 1]) 0 else 1,
// Insert
cost[j] + 1,
// Delete
newCost[j - 1] + 1
)
}
// Swapping costs
cost = newCost.also { newCost = cost }
}
return cost[this.length]
}

View File

@ -0,0 +1,30 @@
/*
* 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
import android.content.Context
import android.content.Intent
import android.net.Uri
/**
* Open given link in browser
*/
fun openLink(mContext: Context, url: String) {
mContext.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)))
}

View File

@ -1,308 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-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
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.sadellie.unitto.data.KEY_COMMA
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_E
import com.sadellie.unitto.data.KEY_LEFT_BRACKET
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
import com.sadellie.unitto.data.OPERATORS
import com.sadellie.unitto.data.preferences.OutputFormat
import com.sadellie.unitto.data.preferences.Separator
import com.sadellie.unitto.data.units.AbstractUnit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.max
object Formatter {
private const val SPACE = " "
private const val PERIOD = "."
private const val COMMA = ","
/**
* Grouping separator.
*/
private var grouping: String = SPACE
/**
* Fractional part separator.
*/
var fractional = KEY_COMMA
/**
* Change current separator to another [separator].
*
* @see [Separator]
*/
fun setSeparator(separator: Int) {
grouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
}
/**
* Format [input].
*
* This will replace operators to their more appealing variants: divide, multiply and minus.
* Plus operator remains unchanged.
*
* Numbers will also be formatted.
*
* @see [formatNumber]
*/
fun format(input: String): String {
// Don't do anything to engineering string.
if (input.contains(KEY_E)) return input.replace(KEY_DOT, fractional)
var output = input
// We may receive expressions
// Find all numbers in that expression
val allNumbers: List<String> = input.split(
*OPERATORS.toTypedArray(), KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET
)
allNumbers.forEach {
output = output.replace(it, formatNumber(it))
}
return output
}
/**
* Format given [input].
*
* Input must be a number. Will replace grouping separators and fractional part separators.
*
* @see grouping
* @see fractional
*/
private fun formatNumber(input: String): String {
val splitInput = input.split(".")
var firstPart = splitInput[0]
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
val offset = 3 - firstPart.length.mod(3)
var output = if (offset != 3) {
// We add some spaces at the begging so that last chunk has 3 symbols
firstPart = " ".repeat(offset) + firstPart
firstPart.chunked(3).joinToString(this.grouping).drop(offset)
} else {
firstPart.chunked(3).joinToString(this.grouping)
}
// Handling fractional part
if (input.contains(".")) {
output = output + fractional + splitInput.getOrElse(1) { "" }
}
return output
}
}
/**
* Shorthand function to use correct `toString` method according to [outputFormat].
*/
fun BigDecimal.toStringWith(outputFormat: Int): String {
// Setting result value using a specified OutputFormat
return when (outputFormat) {
OutputFormat.ALLOW_ENGINEERING -> this.toString()
OutputFormat.FORCE_ENGINEERING -> this.toEngineeringString()
else -> this.toPlainString()
}
}
/**
* Sets the minimum scale that is required to get first non zero value in fractional part
*
* @param[prefScale] Is the preferred scale, the one which will be compared against
*/
fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
/**
* Here we are getting the amount of zeros in fractional part before non zero value
* For example, for 0.00000123456 we need the length of 00000
* Next we add one to get the position of the first non zero value
* Also, this block is only for VERY small numbers
*/
return this.setScale(
max(
prefScale,
if (this.abs() < BigDecimal.ONE) {
// https://stackoverflow.com/a/46136593
-floor(log10(this.abs().remainder(BigDecimal.ONE).toDouble())).toInt()
} else {
0
}
),
RoundingMode.HALF_EVEN
)
}
/**
* Open given link in browser
*/
fun openLink(mContext: Context, url: String) {
mContext.startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)))
}
/**
* Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
* first.
*
* @return The amount of changes that are needed to transform one string into another
*/
fun String.lev(secondString: String): Int {
val stringA = this.lowercase()
val stringB = secondString.lowercase()
// Skipping computation for this cases
if (stringA == stringB) return 0
if (stringA.isEmpty()) return stringB.length
// This case is basically unreal in this app, because secondString is a unit name and they are never empty
if (stringB.isEmpty()) return stringA.length
var cost = IntArray(stringA.length + 1) { it }
var newCost = IntArray(stringA.length + 1)
for (i in 1..stringB.length) {
// basically shifting this to the right by 1 each time
newCost[0] = i
for (j in 1..stringA.length) {
newCost[j] = minOf(
// Adding 1 if they don't match, i.e. need to replace
cost[j - 1] + if (stringA[j - 1] == stringB[i - 1]) 0 else 1,
// Insert
cost[j] + 1,
// Delete
newCost[j - 1] + 1
)
}
// Swapping costs
cost = newCost.also { newCost = cost }
}
return cost[this.length]
}
/**
* Sorts sequence of units by Levenshtein distance
*
* @param stringA String for Levenshtein distance
* @return Sorted sequence of units. Units with lower Levenshtein distance are higher
*/
fun Sequence<AbstractUnit>.sortByLev(stringA: String): Sequence<AbstractUnit> {
val stringToCompare = stringA.lowercase()
// We don't need units where name is too different, half of the symbols is wrong in this situation
val threshold: Int = stringToCompare.length / 2
// List of pair: Unit and it's levDist
val unitsWithDist = mutableListOf<Pair<AbstractUnit, Int>>()
this.forEach { unit ->
val unitName: String = unit.renderedName.lowercase()
/**
* There is chance that unit name doesn't need any edits (contains part of the query)
* So computing levDist is a waste of resources
*/
when {
// It's the best possible match if it start with
unitName.startsWith(stringToCompare) -> {
unitsWithDist.add(Pair(unit, 0))
return@forEach
}
// It's a little bit worse when it just contains part of the query
unitName.contains(stringToCompare) -> {
unitsWithDist.add(Pair(unit, 1))
return@forEach
}
}
/**
* Levenshtein Distance for this specific name of this unit
*
* We use substring so that we compare not the whole unit name, but only part of it.
* It's required because without it levDist will be too high for units with longer
* names than the search query
*
* For example:
* Search query is 'Kelometer' and unit name is 'Kilometer per hour'
* Without substring levDist will be 9 which means that this unit will be skipped
*
* With substring levDist will be 3 so unit will be included
*/
val levDist = unitName
.substring(0, minOf(stringToCompare.length, unitName.length))
.lev(stringToCompare)
// Threshold
if (levDist < threshold) {
unitsWithDist.add(Pair(unit, levDist))
}
}
// Sorting by levDist and getting units
return unitsWithDist
.sortedBy { it.second }
.map { it.first }
.asSequence()
}
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6,) -> R
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6,) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
)
}
/**
* Removes all trailing zeroes.
*
* @throws NumberFormatException if value is bigger than [Double.MAX_VALUE] to avoid memory overflow.
*/
fun BigDecimal.trimZeros(): BigDecimal {
if (this.abs() > BigDecimal.valueOf(Double.MAX_VALUE)) throw NumberFormatException()
return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros()
}

View File

@ -18,12 +18,6 @@
package com.sadellie.unitto.screens.main package com.sadellie.unitto.screens.main
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
@ -40,9 +34,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.sadellie.unitto.R import com.sadellie.unitto.R
@ -59,7 +51,6 @@ fun MainScreen(
viewModel: MainViewModel = viewModel() viewModel: MainViewModel = viewModel()
) { ) {
var launched: Boolean by rememberSaveable { mutableStateOf(false) } var launched: Boolean by rememberSaveable { mutableStateOf(false) }
val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle() val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle()
Scaffold( Scaffold(
@ -82,11 +73,10 @@ fun MainScreen(
) )
}, },
content = { padding -> content = { padding ->
PortraitMainScreenContent( MainScreenContent(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
unitFrom = viewModel.unitFrom, unitFrom = viewModel.unitFrom,
unitTo = viewModel.unitTo, unitTo = viewModel.unitTo,
portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
mainScreenUIState = mainScreenUIState.value, mainScreenUIState = mainScreenUIState.value,
navControllerAction = { navControllerAction(it) }, navControllerAction = { navControllerAction(it) },
swapMeasurements = { viewModel.swapUnits() }, swapMeasurements = { viewModel.swapUnits() },
@ -108,11 +98,10 @@ fun MainScreen(
} }
@Composable @Composable
private fun PortraitMainScreenContent( private fun MainScreenContent(
modifier: Modifier, modifier: Modifier,
unitFrom: AbstractUnit, unitFrom: AbstractUnit,
unitTo: AbstractUnit, unitTo: AbstractUnit,
portrait: Boolean = true,
mainScreenUIState: MainScreenUIState = MainScreenUIState(), mainScreenUIState: MainScreenUIState = MainScreenUIState(),
navControllerAction: (String) -> Unit = {}, navControllerAction: (String) -> Unit = {},
swapMeasurements: () -> Unit = {}, swapMeasurements: () -> Unit = {},
@ -120,14 +109,11 @@ private fun PortraitMainScreenContent(
deleteDigit: () -> Unit = {}, deleteDigit: () -> Unit = {},
clearInput: () -> Unit = {}, clearInput: () -> Unit = {},
) { ) {
if (portrait) { PortraitLandscape(
Column( modifier = modifier,
modifier content1 = {
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TopScreenPart( TopScreenPart(
modifier = Modifier, modifier = it,
inputValue = mainScreenUIState.inputValue, inputValue = mainScreenUIState.inputValue,
calculatedValue = mainScreenUIState.calculatedValue, calculatedValue = mainScreenUIState.calculatedValue,
outputValue = mainScreenUIState.resultValue, outputValue = mainScreenUIState.resultValue,
@ -139,48 +125,14 @@ private fun PortraitMainScreenContent(
onUnitSelectionClick = navControllerAction, onUnitSelectionClick = navControllerAction,
swapUnits = swapMeasurements swapUnits = swapMeasurements
) )
// Keyboard which takes half the screen },
content2 = {
Keyboard( Keyboard(
Modifier modifier = it,
.fillMaxSize()
.padding(horizontal = 8.dp),
addDigit = processInput, addDigit = processInput,
deleteDigit = deleteDigit, deleteDigit = deleteDigit,
clearInput = clearInput, clearInput = clearInput,
) )
} }
} else {
Row(
modifier
.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TopScreenPart(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
inputValue = mainScreenUIState.inputValue,
calculatedValue = mainScreenUIState.calculatedValue,
outputValue = mainScreenUIState.resultValue,
unitFrom = unitFrom,
unitTo = unitTo,
loadingDatabase = mainScreenUIState.isLoadingDatabase,
loadingNetwork = mainScreenUIState.isLoadingNetwork,
networkError = mainScreenUIState.showError,
onUnitSelectionClick = navControllerAction,
swapUnits = swapMeasurements
) )
// Keyboard which takes half the screen
Keyboard(
Modifier
.weight(1f)
.fillMaxSize()
.padding(horizontal = 8.dp),
addDigit = processInput,
deleteDigit = deleteDigit,
clearInput = clearInput,
)
}
}
} }

View File

@ -59,9 +59,9 @@ 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 com.sadellie.unitto.data.units.remote.CurrencyApi import com.sadellie.unitto.data.units.remote.CurrencyApi
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
import com.sadellie.unitto.screens.combine import com.sadellie.unitto.data.combine
import com.sadellie.unitto.screens.toStringWith import com.sadellie.unitto.data.toStringWith
import com.sadellie.unitto.screens.trimZeros import com.sadellie.unitto.data.trimZeros
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel

View File

@ -0,0 +1,74 @@
/*
* 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.main
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
/**
* When Portrait mode will place [content1] and [content2] in a [Column].
*
* When Landscape mode will place [content1] and [content2] in a [Row].
*/
@Composable
fun PortraitLandscape(
modifier: Modifier,
content1: @Composable (Modifier) -> Unit,
content2: @Composable (Modifier) -> Unit,
) {
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
content1(Modifier)
content2(
Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
)
}
} else {
Row(
modifier = modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
content1(
Modifier
.weight(1f)
.fillMaxHeight()
)
content2(
Modifier
.weight(1f)
.fillMaxSize()
.padding(horizontal = 8.dp)
)
}
}
}

View File

@ -51,40 +51,34 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.screens.Formatter
import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayLarge import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayLarge
import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayMedium import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayMedium
/** /**
* Component for input and output * Component for input and output
* *
* @param modifier Modifier that is applied to [LazyRow] * @param modifier Modifier that is applied to [LazyRow].
* @param primaryText Primary text to show (input/output). * @param primaryText Primary text to show (input/output).
* @param secondaryText Secondary text to show (input, calculated result). * @param secondaryText Secondary text to show (input, calculated result).
* @param helperText Helper text below current text (short unit name) * @param helperText Helper text below current text (short unit name).
* @param showLoading Show "Loading" text * @param textToCopy Text that will be copied to clipboard when long-clicking.
* @param showError Show "Error" text
*/ */
@Composable @Composable
fun MyTextField( fun MyTextField(
modifier: Modifier = Modifier, modifier: Modifier,
primaryText: String = String(), primaryText: @Composable () -> String,
secondaryText: String? = null, secondaryText: String?,
helperText: String = String(), helperText: String,
showLoading: Boolean = false, textToCopy: String,
showError: Boolean = false
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val mc = LocalContext.current val mc = LocalContext.current
val textToShow = when { val textToShow: String = primaryText()
showError -> stringResource(R.string.error_label)
showLoading -> stringResource(R.string.loading_label)
else -> Formatter.format(primaryText)
}
val copiedText: String = val copiedText: String =
stringResource(R.string.copied, secondaryText?.let { Formatter.format(it) } ?: textToShow) stringResource(R.string.copied, textToCopy)
Column(modifier = Modifier Column(
modifier = Modifier
.combinedClickable( .combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(), indication = rememberRipple(),
@ -153,7 +147,7 @@ fun MyTextField(
Text( Text(
modifier = Modifier, modifier = Modifier,
// Quick fix to prevent the UI from crashing // Quick fix to prevent the UI from crashing
text = Formatter.format(it?.take(1000) ?: ""), text = it?.take(1000) ?: "",
textAlign = TextAlign.End, textAlign = TextAlign.End,
softWrap = false, softWrap = false,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),

View File

@ -44,6 +44,7 @@ import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN
import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.screens.Formatter
/** /**
* Top of the main screen. Contains input and output TextFields, and unit selection row of buttons. * Top of the main screen. Contains input and output TextFields, and unit selection row of buttons.
@ -88,20 +89,30 @@ fun TopScreenPart(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
MyTextField( MyTextField(
Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
inputValue, primaryText = {
calculatedValue, when {
stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName), loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
loadingNetwork, networkError -> stringResource(R.string.error_label)
networkError else -> Formatter.format(inputValue)
}
},
secondaryText = calculatedValue?.let { Formatter.format(it) },
helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName),
textToCopy = calculatedValue ?: inputValue,
) )
MyTextField( MyTextField(
Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
outputValue, primaryText = {
null, when {
stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName), loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
loadingNetwork, networkError -> stringResource(R.string.error_label)
networkError else -> Formatter.format(outputValue)
}
},
secondaryText = null,
helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName),
textToCopy = outputValue,
) )
// Unit selection buttons // Unit selection buttons
Row( Row(

View File

@ -21,6 +21,7 @@ package com.sadellie.unitto.screens
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.MyUnit import com.sadellie.unitto.data.units.MyUnit
import com.sadellie.unitto.data.units.UnitGroup import com.sadellie.unitto.data.units.UnitGroup
import com.sadellie.unitto.data.units.sortByLev
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.screens package com.sadellie.unitto.screens
import com.sadellie.unitto.data.setMinimumRequiredScale
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.math.BigDecimal import java.math.BigDecimal