mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Not so little UI refactor.
Refactored Utilities. Refactored MainScreen. Now using a better approach for portrait/landscape. Refactored TextField.
This commit is contained in:
parent
7105ee90b6
commit
24c407a05d
@ -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()
|
||||||
|
}
|
42
app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
Normal file
42
app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
116
app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
Normal file
116
app/src/main/java/com/sadellie/unitto/screens/Formatter.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
60
app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
Normal file
60
app/src/main/java/com/sadellie/unitto/screens/StringUtils.kt
Normal 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]
|
||||||
|
}
|
30
app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
Normal file
30
app/src/main/java/com/sadellie/unitto/screens/UIUtils.kt
Normal 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)))
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user