diff --git a/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt b/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt new file mode 100644 index 00000000..b7fca45e --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/data/BigDecimalUtils.kt @@ -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 . + */ + +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() +} diff --git a/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt new file mode 100644 index 00000000..77b2533c --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt @@ -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 . + */ + +package com.sadellie.unitto.data + +import kotlinx.coroutines.flow.Flow + +@Suppress("UNCHECKED_CAST") +fun combine( + flow: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6) -> R +): Flow = + 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, + ) + } diff --git a/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt b/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt index 8f058ce6..bcc2fb79 100644 --- a/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt +++ b/app/src/main/java/com/sadellie/unitto/data/units/AbstractUnit.kt @@ -19,6 +19,7 @@ package com.sadellie.unitto.data.units import androidx.annotation.StringRes +import com.sadellie.unitto.screens.lev import java.math.BigDecimal /** @@ -64,3 +65,64 @@ abstract class AbstractUnit( scale: Int ): 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.sortByLev(stringA: String): Sequence { + 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>() + 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() +} diff --git a/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt b/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt index 007d1e1a..c376c098 100644 --- a/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt +++ b/app/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt @@ -21,10 +21,9 @@ package com.sadellie.unitto.data.units import android.content.Context import com.sadellie.unitto.R 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.screens.setMinimumRequiredScale -import com.sadellie.unitto.screens.sortByLev -import com.sadellie.unitto.screens.trimZeros import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject diff --git a/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt b/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt index ce6c2696..5beb2373 100644 --- a/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt +++ b/app/src/main/java/com/sadellie/unitto/data/units/MyUnit.kt @@ -20,8 +20,8 @@ package com.sadellie.unitto.data.units import androidx.annotation.StringRes import com.sadellie.unitto.data.preferences.MAX_PRECISION -import com.sadellie.unitto.screens.setMinimumRequiredScale -import com.sadellie.unitto.screens.trimZeros +import com.sadellie.unitto.data.setMinimumRequiredScale +import com.sadellie.unitto.data.trimZeros import java.math.BigDecimal /** diff --git a/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt new file mode 100644 index 00000000..c94b1a63 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt @@ -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 . + */ + +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 = 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 + } +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt b/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt new file mode 100644 index 00000000..a83e9df0 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt @@ -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 . + */ + +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] +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt b/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt new file mode 100644 index 00000000..291774b3 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt @@ -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 . + */ + +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))) +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/Utils.kt b/app/src/main/java/com/sadellie/unitto/screens/Utils.kt deleted file mode 100644 index d9583431..00000000 --- a/app/src/main/java/com/sadellie/unitto/screens/Utils.kt +++ /dev/null @@ -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 . - */ - -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 = 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.sortByLev(stringA: String): Sequence { - 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>() - 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 combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6,) -> R -): 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, - ) -} - -/** - * 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() -} diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt index 3ce3cc98..5137a6be 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt @@ -18,12 +18,6 @@ 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.material.icons.Icons 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.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.sadellie.unitto.R @@ -59,7 +51,6 @@ fun MainScreen( viewModel: MainViewModel = viewModel() ) { var launched: Boolean by rememberSaveable { mutableStateOf(false) } - val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle() Scaffold( @@ -82,11 +73,10 @@ fun MainScreen( ) }, content = { padding -> - PortraitMainScreenContent( + MainScreenContent( modifier = Modifier.padding(padding), unitFrom = viewModel.unitFrom, unitTo = viewModel.unitTo, - portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT, mainScreenUIState = mainScreenUIState.value, navControllerAction = { navControllerAction(it) }, swapMeasurements = { viewModel.swapUnits() }, @@ -108,11 +98,10 @@ fun MainScreen( } @Composable -private fun PortraitMainScreenContent( +private fun MainScreenContent( modifier: Modifier, unitFrom: AbstractUnit, unitTo: AbstractUnit, - portrait: Boolean = true, mainScreenUIState: MainScreenUIState = MainScreenUIState(), navControllerAction: (String) -> Unit = {}, swapMeasurements: () -> Unit = {}, @@ -120,14 +109,11 @@ private fun PortraitMainScreenContent( deleteDigit: () -> Unit = {}, clearInput: () -> Unit = {}, ) { - if (portrait) { - Column( - modifier - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + PortraitLandscape( + modifier = modifier, + content1 = { TopScreenPart( - modifier = Modifier, + modifier = it, inputValue = mainScreenUIState.inputValue, calculatedValue = mainScreenUIState.calculatedValue, outputValue = mainScreenUIState.resultValue, @@ -139,48 +125,14 @@ private fun PortraitMainScreenContent( onUnitSelectionClick = navControllerAction, swapUnits = swapMeasurements ) - // Keyboard which takes half the screen + }, + content2 = { Keyboard( - Modifier - .fillMaxSize() - .padding(horizontal = 8.dp), + modifier = it, addDigit = processInput, deleteDigit = deleteDigit, 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, - ) - } - } + ) } diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt index 097614d8..e77ea5b7 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt @@ -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.remote.CurrencyApi import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse -import com.sadellie.unitto.screens.combine -import com.sadellie.unitto.screens.toStringWith -import com.sadellie.unitto.screens.trimZeros +import com.sadellie.unitto.data.combine +import com.sadellie.unitto.data.toStringWith +import com.sadellie.unitto.data.trimZeros import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt b/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt new file mode 100644 index 00000000..a9964392 --- /dev/null +++ b/app/src/main/java/com/sadellie/unitto/screens/main/PortraitLandscape.kt @@ -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 . + */ + +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) + ) + } + } +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt index a4609b3a..991bbd1d 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/MyTextField.kt @@ -51,51 +51,45 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sadellie.unitto.R -import com.sadellie.unitto.screens.Formatter import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayLarge import com.sadellie.unitto.ui.theme.NumbersTextStyleDisplayMedium /** * 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 secondaryText Secondary text to show (input, calculated result). - * @param helperText Helper text below current text (short unit name) - * @param showLoading Show "Loading" text - * @param showError Show "Error" text + * @param helperText Helper text below current text (short unit name). + * @param textToCopy Text that will be copied to clipboard when long-clicking. */ @Composable fun MyTextField( - modifier: Modifier = Modifier, - primaryText: String = String(), - secondaryText: String? = null, - helperText: String = String(), - showLoading: Boolean = false, - showError: Boolean = false + modifier: Modifier, + primaryText: @Composable () -> String, + secondaryText: String?, + helperText: String, + textToCopy: String, ) { val clipboardManager = LocalClipboardManager.current val mc = LocalContext.current - val textToShow = when { - showError -> stringResource(R.string.error_label) - showLoading -> stringResource(R.string.loading_label) - else -> Formatter.format(primaryText) - } + val textToShow: String = primaryText() val copiedText: String = - stringResource(R.string.copied, secondaryText?.let { Formatter.format(it) } ?: textToShow) + stringResource(R.string.copied, textToCopy) - Column(modifier = Modifier - .combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), - onClick = {}, - onLongClick = { - clipboardManager.setText(AnnotatedString(secondaryText ?: textToShow)) - Toast - .makeText(mc, copiedText, Toast.LENGTH_SHORT) - .show() - } - ) + Column( + modifier = Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = {}, + onLongClick = { + clipboardManager.setText(AnnotatedString(secondaryText ?: textToShow)) + Toast + .makeText(mc, copiedText, Toast.LENGTH_SHORT) + .show() + } + ) ) { LazyRow( modifier = modifier @@ -153,7 +147,7 @@ fun MyTextField( Text( modifier = Modifier, // Quick fix to prevent the UI from crashing - text = Formatter.format(it?.take(1000) ?: ""), + text = it?.take(1000) ?: "", textAlign = TextAlign.End, softWrap = false, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt index ea69eb4b..bee0e89e 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt @@ -44,6 +44,7 @@ import com.sadellie.unitto.R import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN 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. @@ -88,20 +89,30 @@ fun TopScreenPart( verticalArrangement = Arrangement.spacedBy(8.dp) ) { MyTextField( - Modifier.fillMaxWidth(), - inputValue, - calculatedValue, - stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName), - loadingNetwork, - networkError + modifier = Modifier.fillMaxWidth(), + primaryText = { + when { + loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label) + networkError -> stringResource(R.string.error_label) + 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( - Modifier.fillMaxWidth(), - outputValue, - null, - stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName), - loadingNetwork, - networkError + modifier = Modifier.fillMaxWidth(), + primaryText = { + when { + loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label) + networkError -> stringResource(R.string.error_label) + else -> Formatter.format(outputValue) + } + }, + secondaryText = null, + helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName), + textToCopy = outputValue, ) // Unit selection buttons Row( diff --git a/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt b/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt index ce19fd40..54c7a72b 100644 --- a/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt +++ b/app/src/test/java/com/sadellie/unitto/screens/LevenshteinFilterAndSortTest.kt @@ -21,6 +21,7 @@ package com.sadellie.unitto.screens import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.MyUnit import com.sadellie.unitto.data.units.UnitGroup +import com.sadellie.unitto.data.units.sortByLev import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigDecimal diff --git a/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt b/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt index 4facd9b7..3982b0b5 100644 --- a/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt +++ b/app/src/test/java/com/sadellie/unitto/screens/MinimumRequiredScaleTest.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.screens +import com.sadellie.unitto.data.setMinimumRequiredScale import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigDecimal