diff --git a/FUNDING.md b/FUNDING.md
deleted file mode 100644
index 584a724d..00000000
--- a/FUNDING.md
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-I don't need your money, just help Unitto gain **more users**:
-- Tell your relatives
-- Tell your friends
-- Tell strangers on the streets
-- Spread the word, take over the world... or something like that
diff --git a/README.md b/README.md
index 5ac6939a..5b338eb8 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,7 @@
## đČ Download
-
-
+
## đ Features
- **Instant** expression evaluation
@@ -29,25 +28,21 @@
- Customizable number **formatter**
- **SI Standard**
-## đ
Translate
-
+## đ
[Translate](https://poeditor.com/join/project/T4zjmoq8dx)
+Join on **POEditor** to help.
-## đ€ Donate
-Visit [FUNDING.md](./FUNDING.md)
+## đĄ [Open issues](https://github.com/sadellie/unitto/issues/new)
+Report bugs or request improvements. I may close your issue as not planned and reopen it later (things change).
+
+## đ€ [Start discussions](https://github.com/sadellie/unitto/discussions/new/choose)
+If you think that your question will not fit in "Issues", start a discussion.
+
+## đ©âđ» ~~Contribute code~~
+Code contributions are **not** welcomed. If you really want to, **ask me** first.
+
+Hard forks and alterations of Unitto are **not** welcomed. Use a "Fork" button so that commits' author is not lost.
## đ Additional
-
-
-
-
-
+Terms and Conditions: https://sadellie.github.io/unitto/terms
-
-
-Terms and Conditions, Privacy Policy, Press Kit and contact links:
-https://sadellie.github.io/unitto/
-
-## đ€ Technical details
-- App is written in Compose
-- Multi-module architecture
-- Convention plugins for modules
\ No newline at end of file
+Privacy Policy: https://sadellie.github.io/unitto/privacy
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a4eb1c44..ab00e8a9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -16,6 +16,8 @@
* along with this program. If not, see .
*/
+@file:Suppress("UnstableApiUsage")
+
plugins {
// Basic stuff
id("com.android.application")
@@ -26,14 +28,14 @@ plugins {
android {
namespace = "com.sadellie.unitto"
- compileSdk = 33
+ compileSdk = 34
defaultConfig {
applicationId = "com.sadellie.unitto"
minSdk = 21
- targetSdk = 33
- versionCode = 20
- versionName = "Kobicha"
+ targetSdk = 34
+ versionCode = 22
+ versionName = "Lilac Luster"
}
buildTypes {
@@ -74,19 +76,19 @@ android {
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
- packagingOptions {
+ packaging {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi"
)
@@ -97,6 +99,12 @@ android {
}
}
+tasks.withType().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+}
+
dependencies {
implementation(libs.androidx.core)
coreLibraryDesugaring(libs.android.desugarJdkLibs)
@@ -112,9 +120,8 @@ dependencies {
implementation(project(mapOf("path" to ":feature:calculator")))
implementation(project(mapOf("path" to ":feature:settings")))
implementation(project(mapOf("path" to ":feature:unitslist")))
- implementation(project(mapOf("path" to ":feature:epoch")))
+ implementation(project(mapOf("path" to ":feature:datedifference")))
implementation(project(mapOf("path" to ":feature:timezone")))
- implementation(project(mapOf("path" to ":data:units")))
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":core:ui")))
diff --git a/app/src/main/java/com/sadellie/unitto/MainActivity.kt b/app/src/main/java/com/sadellie/unitto/MainActivity.kt
index 9516e8a5..b68879ed 100644
--- a/app/src/main/java/com/sadellie/unitto/MainActivity.kt
+++ b/app/src/main/java/com/sadellie/unitto/MainActivity.kt
@@ -41,10 +41,10 @@ internal class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
- val userPrefs = userPrefsRepository.userPreferencesFlow
+ val uiPrefs = userPrefsRepository.uiPreferencesFlow
.collectAsStateWithLifecycle(null).value
- if (userPrefs != null) UnittoApp(userPrefs)
+ if (uiPrefs != null) UnittoApp(uiPrefs)
}
}
diff --git a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
index 07ca79bc..224f872a 100644
--- a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
+++ b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
@@ -18,11 +18,10 @@
package com.sadellie.unitto
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween
-import androidx.compose.material3.DrawerValue
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalNavigationDrawer
-import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -40,35 +39,44 @@ import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.core.ui.common.UnittoDrawerSheet
+import com.sadellie.unitto.core.ui.common.UnittoModalNavigationDrawer
+import com.sadellie.unitto.core.ui.common.close
+import com.sadellie.unitto.core.ui.common.isOpen
+import com.sadellie.unitto.core.ui.common.open
+import com.sadellie.unitto.core.ui.common.rememberUnittoDrawerState
import com.sadellie.unitto.core.ui.model.DrawerItems
import com.sadellie.unitto.core.ui.theme.AppTypography
import com.sadellie.unitto.core.ui.theme.DarkThemeColors
import com.sadellie.unitto.core.ui.theme.LightThemeColors
-import com.sadellie.unitto.data.userprefs.UserPreferences
+import com.sadellie.unitto.data.userprefs.UIPreferences
import io.github.sadellie.themmo.Themmo
import io.github.sadellie.themmo.rememberThemmoController
import kotlinx.coroutines.launch
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-internal fun UnittoApp(userPrefs: UserPreferences) {
+internal fun UnittoApp(uiPrefs: UIPreferences) {
val themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
- // Anything below will not be called if theming mode is still loading from DataStore
- themingMode = userPrefs.themingMode,
- dynamicThemeEnabled = userPrefs.enableDynamicTheme,
- amoledThemeEnabled = userPrefs.enableAmoledTheme,
- customColor = userPrefs.customColor,
- monetMode = userPrefs.monetMode
+ themingMode = uiPrefs.themingMode,
+ dynamicThemeEnabled = uiPrefs.enableDynamicTheme,
+ amoledThemeEnabled = uiPrefs.enableAmoledTheme,
+ customColor = uiPrefs.customColor,
+ monetMode = uiPrefs.monetMode
)
val navController = rememberNavController()
val sysUiController = rememberSystemUiController()
// Navigation drawer stuff
- val drawerState = rememberDrawerState(DrawerValue.Closed)
+ val drawerState = rememberUnittoDrawerState()
val drawerScope = rememberCoroutineScope()
- val mainTabs = listOf(DrawerItems.Calculator, DrawerItems.Converter)
+ val mainTabs = listOf(
+ DrawerItems.Calculator,
+ DrawerItems.Converter,
+ DrawerItems.DateDifference
+ )
val additionalTabs = listOf(DrawerItems.Settings)
val navBackStackEntry by navController.currentBackStackEntryAsState()
@@ -84,14 +92,6 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
}
}
}
- val gesturesEnabled: Boolean by remember(navBackStackEntry?.destination) {
- derivedStateOf {
- // Will be true for routes like
- // [null, calculator_route, settings_graph, settings_route, themes_route]
- // We disable drawer drag gesture when we are too deep
- navController.backQueue.size <= 4
- }
- }
Themmo(
themmoController = themmoController,
@@ -103,10 +103,8 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
mutableStateOf(backgroundColor.luminance() > 0.5f)
}
- ModalNavigationDrawer(
- drawerState = drawerState,
- gesturesEnabled = gesturesEnabled,
- drawerContent = {
+ UnittoModalNavigationDrawer(
+ drawer = {
UnittoDrawerSheet(
modifier = Modifier,
mainTabs = mainTabs,
@@ -122,14 +120,23 @@ internal fun UnittoApp(userPrefs: UserPreferences) {
restoreState = true
}
}
+ },
+ modifier = Modifier,
+ state = drawerState,
+ gesturesEnabled = true,
+ scope = drawerScope,
+ content = {
+ UnittoNavigation(
+ navController = navController,
+ themmoController = it,
+ startDestination = uiPrefs.startingScreen,
+ openDrawer = { drawerScope.launch { drawerState.open() } }
+ )
}
- ) {
- UnittoNavigation(
- navController = navController,
- themmoController = it,
- startDestination = userPrefs.startingScreen,
- openDrawer = { drawerScope.launch { drawerState.open() } }
- )
+ )
+
+ BackHandler(drawerState.isOpen) {
+ drawerScope.launch { drawerState.close() }
}
LaunchedEffect(useDarkIcons) {
diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
index 003cc061..f52e1fe2 100644
--- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
+++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
@@ -23,14 +23,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.sadellie.unitto.feature.calculator.navigation.calculatorScreen
import com.sadellie.unitto.feature.converter.ConverterViewModel
import com.sadellie.unitto.feature.converter.navigation.converterScreen
-import com.sadellie.unitto.feature.epoch.navigation.epochScreen
-import com.sadellie.unitto.feature.settings.SettingsViewModel
+import com.sadellie.unitto.feature.datedifference.navigation.dateDifferenceScreen
import com.sadellie.unitto.feature.settings.navigation.navigateToSettings
import com.sadellie.unitto.feature.settings.navigation.navigateToUnitGroups
import com.sadellie.unitto.feature.settings.navigation.settingGraph
@@ -51,27 +49,16 @@ internal fun UnittoNavigation(
) {
val converterViewModel: ConverterViewModel = hiltViewModel()
val unitsListViewModel: UnitsListViewModel = hiltViewModel()
- val settingsViewModel: SettingsViewModel = hiltViewModel()
NavHost(
navController = navController,
startDestination = startDestination,
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) {
- fun navigateToSettings() {
- navController.navigateToSettings {
- popUpTo(navController.graph.findStartDestination().id) {
- saveState = true
- }
- launchSingleTop = true
- restoreState = true
- }
- }
-
converterScreen(
navigateToLeftScreen = navController::navigateToLeftSide,
navigateToRightScreen = navController::navigateToRightSide,
- navigateToSettings = ::navigateToSettings,
+ navigateToSettings = navController::navigateToSettings,
navigateToMenu = openDrawer,
viewModel = converterViewModel
)
@@ -91,7 +78,6 @@ internal fun UnittoNavigation(
)
settingGraph(
- settingsViewModel = settingsViewModel,
themmoController = themmoController,
navController = navController,
menuButtonClick = openDrawer
@@ -99,14 +85,17 @@ internal fun UnittoNavigation(
calculatorScreen(
navigateToMenu = openDrawer,
- navigateToSettings = ::navigateToSettings
+ navigateToSettings = navController::navigateToSettings
)
- epochScreen(navigateToMenu = openDrawer)
+ dateDifferenceScreen(
+ navigateToMenu = openDrawer,
+ navigateToSettings = navController::navigateToSettings
+ )
timeZoneScreen(
navigateToMenu = openDrawer,
- navigateToSettings = ::navigateToSettings
+ navigateToSettings = navController::navigateToSettings
)
}
}
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 05926ed7..b3014624 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -27,6 +27,12 @@ java {
targetCompatibility = JavaVersion.VERSION_11
}
+tasks.withType().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+}
+
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
diff --git a/build-logic/convention/src/main/java/UnittoHiltPlugin.kt b/build-logic/convention/src/main/java/UnittoHiltPlugin.kt
index f883aa1d..be0545f1 100644
--- a/build-logic/convention/src/main/java/UnittoHiltPlugin.kt
+++ b/build-logic/convention/src/main/java/UnittoHiltPlugin.kt
@@ -22,6 +22,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
+@Suppress("UNUSED")
class UnittoHiltPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/java/UnittoLibraryComposePlugin.kt b/build-logic/convention/src/main/java/UnittoLibraryComposePlugin.kt
index 37a429f2..d7670ac2 100644
--- a/build-logic/convention/src/main/java/UnittoLibraryComposePlugin.kt
+++ b/build-logic/convention/src/main/java/UnittoLibraryComposePlugin.kt
@@ -25,6 +25,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
+@Suppress("UNUSED")
class UnittoLibraryComposePlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt b/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt
index 49b4e58d..26c031fb 100644
--- a/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt
+++ b/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt
@@ -22,6 +22,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
+@Suppress("UNUSED")
class UnittoLibraryFeaturePlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/java/UnittoLibraryPlugin.kt b/build-logic/convention/src/main/java/UnittoLibraryPlugin.kt
index 4e1bc798..b0189e6c 100644
--- a/build-logic/convention/src/main/java/UnittoLibraryPlugin.kt
+++ b/build-logic/convention/src/main/java/UnittoLibraryPlugin.kt
@@ -25,6 +25,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
+@Suppress("UNUSED")
class UnittoLibraryPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureCompose.kt b/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureCompose.kt
index 74dfe304..a97e347d 100644
--- a/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureCompose.kt
+++ b/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureCompose.kt
@@ -23,6 +23,7 @@ import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
+@Suppress("UnstableApiUsage")
internal fun Project.configureCompose(
commonExtension: CommonExtension<*, *, *, *>,
) {
diff --git a/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureKotlinAndroid.kt b/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureKotlinAndroid.kt
index 1bf9d2cc..99c0f09b 100644
--- a/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureKotlinAndroid.kt
+++ b/build-logic/convention/src/main/java/com/sadellie/unitto/ConfigureKotlinAndroid.kt
@@ -25,13 +25,16 @@ import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.ExtensionAware
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+@Suppress("UnstableApiUsage")
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *>,
) {
commonExtension.apply {
- compileSdk = 33
+ compileSdk = 34
defaultConfig {
minSdk = 21
@@ -45,8 +48,8 @@ internal fun Project.configureKotlinAndroid(
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
@@ -59,7 +62,7 @@ internal fun Project.configureKotlinAndroid(
resValues = false
}
- packagingOptions {
+ packaging {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
@@ -73,7 +76,14 @@ internal fun Project.configureKotlinAndroid(
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi"
)
- jvmTarget = JavaVersion.VERSION_1_8.toString()
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+ }
+
+ tasks.withType().configureEach {
+ kotlinOptions {
+ // Set JVM target to 11
+ jvmTarget = JavaVersion.VERSION_11.toString()
}
}
diff --git a/build-logic/gradle/wrapper/gradle-wrapper.jar b/build-logic/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index e708b1c0..00000000
Binary files a/build-logic/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 5ff2e605..00000000
--- a/build-logic/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
-distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index b92e00da..9effadbc 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
google()
diff --git a/content/appGallery.png b/content/appGallery.png
deleted file mode 100644
index 9980f49d..00000000
Binary files a/content/appGallery.png and /dev/null differ
diff --git a/content/donate.png b/content/donate.png
deleted file mode 100644
index 7559d12c..00000000
Binary files a/content/donate.png and /dev/null differ
diff --git a/content/nashStore.png b/content/nashStore.png
deleted file mode 100644
index 55a0ea4e..00000000
Binary files a/content/nashStore.png and /dev/null differ
diff --git a/content/poeditor.png b/content/poeditor.png
deleted file mode 100644
index bc22c0d1..00000000
Binary files a/content/poeditor.png and /dev/null differ
diff --git a/content/progress.png b/content/progress.png
deleted file mode 100644
index 7a6c4e9b..00000000
Binary files a/content/progress.png and /dev/null differ
diff --git a/content/ruMarket.png b/content/ruMarket.png
deleted file mode 100644
index 05b7060b..00000000
Binary files a/content/ruMarket.png and /dev/null differ
diff --git a/content/ruStore.png b/content/ruStore.png
deleted file mode 100644
index c72ea833..00000000
Binary files a/content/ruStore.png and /dev/null differ
diff --git a/content/unittoModules.png b/content/unittoModules.png
deleted file mode 100644
index ddbfe1a6..00000000
Binary files a/content/unittoModules.png and /dev/null differ
diff --git a/core/base/build.gradle.kts b/core/base/build.gradle.kts
index 1ee78dca..e3c29da2 100644
--- a/core/base/build.gradle.kts
+++ b/core/base/build.gradle.kts
@@ -16,6 +16,8 @@
* along with this program. If not, see .
*/
+@file:Suppress("UnstableApiUsage")
+
plugins {
id("unitto.library")
}
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/OutputFormat.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/OutputFormat.kt
index 77d1b9b6..c3002585 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/OutputFormat.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/OutputFormat.kt
@@ -29,14 +29,3 @@ object OutputFormat {
// App will try it's best to use engineering notation
const val FORCE_ENGINEERING = 2
}
-
-/**
- * Available formats. Used in settings
- */
-val OUTPUT_FORMAT: Map by lazy {
- mapOf(
- OutputFormat.PLAIN to R.string.plain,
- OutputFormat.ALLOW_ENGINEERING to R.string.allow_engineering,
- OutputFormat.FORCE_ENGINEERING to R.string.force_engineering,
- )
-}
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/Precision.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/Precision.kt
index cc627f53..f929479d 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/Precision.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/Precision.kt
@@ -22,28 +22,3 @@ package com.sadellie.unitto.core.base
* Current maximum scale that will be used in app. Used in various place in code
*/
const val MAX_PRECISION: Int = 1_000
-
-/**
- * Currently available scale options
- */
-val PRECISIONS: Map by lazy {
- mapOf(
- 0 to R.string.precision_zero,
- 1 to R.string.precision_one,
- 2 to R.string.precision_two,
- 3 to R.string.precision_three,
- 4 to R.string.precision_four,
- 5 to R.string.precision_five,
- 6 to R.string.precision_six,
- 7 to R.string.precision_seven,
- 8 to R.string.precision_eight,
- 9 to R.string.precision_nine,
- 10 to R.string.precision_ten,
- 11 to R.string.precision_eleven,
- 12 to R.string.precision_twelve,
- 13 to R.string.precision_thirteen,
- 14 to R.string.precision_fourteen,
- 15 to R.string.precision_fifteen,
- MAX_PRECISION to R.string.max_precision
- )
-}
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/Separator.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/Separator.kt
index f39294a7..074f8308 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/Separator.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/Separator.kt
@@ -22,18 +22,7 @@ package com.sadellie.unitto.core.base
* Separators mean symbols that separate fractional part
*/
object Separator {
- const val SPACES = 0
+ const val SPACE = 0
const val PERIOD = 1
const val COMMA = 2
}
-
-/**
- * Map of separators that is used in settings
- */
-val SEPARATORS: Map by lazy {
- mapOf(
- Separator.SPACES to R.string.spaces,
- Separator.PERIOD to R.string.period,
- Separator.COMMA to R.string.comma
- )
-}
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt
index 8ea12f19..dd20a0c8 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt
@@ -19,103 +19,126 @@
package com.sadellie.unitto.core.base
object Token {
- const val _1 = "1"
- const val _2 = "2"
- const val _3 = "3"
- const val _4 = "4"
- const val _5 = "5"
- const val _6 = "6"
- const val _7 = "7"
- const val _8 = "8"
- const val _9 = "9"
- const val _0 = "0"
+ object Digit {
+ const val _1 = "1"
+ const val _2 = "2"
+ const val _3 = "3"
+ const val _4 = "4"
+ const val _5 = "5"
+ const val _6 = "6"
+ const val _7 = "7"
+ const val _8 = "8"
+ const val _9 = "9"
+ const val _0 = "0"
+ const val dot = "."
- const val baseA = "A"
- const val baseB = "B"
- const val baseC = "C"
- const val baseD = "D"
- const val baseE = "E"
- const val baseF = "F"
-
- const val dot = "."
- const val comma = ","
- const val E = "E"
-
- const val plus = "+"
- const val minus = "-"
- const val minusDisplay = "â"
-
- const val divide = "/"
- const val divideDisplay = "Ă·"
-
- const val multiply = "*"
- const val multiplyDisplay = "Ă"
-
- const val leftBracket = "("
- const val rightBracket = ")"
- const val exponent = "^"
- const val sqrt = "â"
- const val pi = "Ï"
- const val factorial = "!"
- const val sin = "sin("
- const val arSin = "arsin("
- const val cos = "cos("
- const val arCos = "arcos("
- const val tan = "tan("
- const val acTan = "actan("
- const val e = "e"
- const val exp = "exp("
- const val modulo = "#"
- const val ln = "ln("
- const val log = "log("
- const val percent = "%"
-
- val operators by lazy {
- listOf(
- plus,
- minus,
- minusDisplay,
- multiply,
- multiplyDisplay,
- divide,
- divideDisplay,
- sqrt,
- exponent,
- )
+ val all by lazy {
+ listOf(_1, _2, _3, _4, _5, _6, _7, _8, _9, _0)
+ }
+ val allWithDot by lazy { all + dot }
}
- val digits by lazy {
- listOf(
- _1,
- _2,
- _3,
- _4,
- _5,
- _6,
- _7,
- _8,
- _9,
- _0,
- )
+ object Letter {
+ const val _A = "A"
+ const val _B = "B"
+ const val _C = "C"
+ const val _D = "D"
+ const val _E = "E"
+ const val _F = "F"
+
+ val all by lazy {
+ listOf(_A, _B, _C, _D, _E, _F)
+ }
}
- val internalToDisplay: Map = hashMapOf(
- minus to minusDisplay,
- multiply to multiplyDisplay,
- divide to divideDisplay
- )
+ object Operator {
+ const val plus = "+"
+ const val minus = "â"
+ const val multiply = "Ă"
+ const val divide = "Ă·"
+ const val leftBracket = "("
+ const val rightBracket = ")"
+ const val power = "^"
+ const val factorial = "!"
+ const val modulo = "#"
+ const val percent = "%"
+ const val sqrt = "â"
- val knownSymbols: List by lazy {
- listOf(
- arSin, arCos, acTan, exp,
- sin, cos, tan, ln, log,
- leftBracket, rightBracket,
- exponent, sqrt, factorial,
- modulo, e, percent, pi,
- multiply, multiplyDisplay,
- plus, minus, minusDisplay, divide, divideDisplay,
- baseA, baseB, baseC, baseD, baseE, baseF,
- _1, _2, _3, _4, _5, _6, _7, _8, _9, _0
- ).sortedByDescending { it.length }
+ val all by lazy {
+ listOf(
+ plus, minus, multiply, divide,
+ leftBracket, rightBracket,
+ power, factorial, modulo, percent, sqrt,
+ )
+ }
+ }
+
+ object Func {
+ const val sin = "sin"
+ const val sinBracket = "$sin("
+ const val cos = "cos"
+ const val cosBracket = "$cos("
+ const val tan = "tan"
+ const val tanBracket = "$tan("
+ const val arsin = "sinâ»Âč"
+ const val arsinBracket = "$arsin("
+ const val arcos = "cosâ»Âč"
+ const val arcosBracket = "$arcos("
+ const val actan = "tanâ»Âč"
+ const val actanBracket = "$actan("
+ const val ln = "ln"
+ const val lnBracket = "$ln("
+ const val log = "log"
+ const val logBracket = "$log("
+ const val exp = "exp"
+ const val expBracket = "$exp("
+
+ val all by lazy {
+ listOf(
+ arsin, arcos, actan, sin, cos, tan, log, exp, ln
+ ).sortedByDescending { it.length }
+ }
+
+ val allWithOpeningBracket by lazy {
+ listOf(
+ arsinBracket, arcosBracket, actanBracket, sinBracket, cosBracket, tanBracket,
+ logBracket, expBracket, lnBracket
+ )
+ }
+ }
+
+ object Const {
+ const val pi = "Ï"
+ const val e = "e"
+
+ val all by lazy {
+ listOf(pi, e)
+ }
+ }
+
+ // Used only in formatter, don't use internally
+ object DisplayOnly {
+ const val comma = ","
+ const val engineeringE = "E"
+ const val minus = "â"
+ }
+
+ val expressionTokens by lazy {
+ Digit.allWithDot + Operator.all + Func.all + Const.all
+ }
+
+ val numberBaseTokens by lazy {
+ Digit.all + Letter.all
+ }
+
+ val sexyToUgly by lazy {
+ mapOf(
+ Operator.minus to listOf("-", "â", "â", "â"),
+ Operator.divide to listOf("/"),
+ Operator.multiply to listOf("*", "âą"),
+ Func.arsin to listOf("arsin"),
+ Func.arcos to listOf("arcos"),
+ Func.actan to listOf("actan")
+ )
}
}
diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt
index 967a6d9f..6ee4bad5 100644
--- a/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt
+++ b/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt
@@ -34,9 +34,9 @@ sealed class TopLevelDestinations(
name = R.string.calculator
)
- object Epoch : TopLevelDestinations(
- route = "epoch_route",
- name = R.string.epoch_converter
+ object DateDifference : TopLevelDestinations(
+ route = "date_difference_route",
+ name = R.string.date_difference
)
object TimeZone : TopLevelDestinations(
diff --git a/core/base/src/main/res/mipmap-hdpi/ic_launcher_icon_round.png b/core/base/src/main/res/mipmap-hdpi/ic_launcher_icon_round.png
deleted file mode 100644
index 58645901..00000000
Binary files a/core/base/src/main/res/mipmap-hdpi/ic_launcher_icon_round.png and /dev/null differ
diff --git a/core/base/src/main/res/mipmap-mdpi/ic_launcher_icon_round.png b/core/base/src/main/res/mipmap-mdpi/ic_launcher_icon_round.png
deleted file mode 100644
index 092f58e2..00000000
Binary files a/core/base/src/main/res/mipmap-mdpi/ic_launcher_icon_round.png and /dev/null differ
diff --git a/core/base/src/main/res/mipmap-xhdpi/ic_launcher_icon_round.png b/core/base/src/main/res/mipmap-xhdpi/ic_launcher_icon_round.png
deleted file mode 100644
index ea63bc68..00000000
Binary files a/core/base/src/main/res/mipmap-xhdpi/ic_launcher_icon_round.png and /dev/null differ
diff --git a/core/base/src/main/res/mipmap-xxhdpi/ic_launcher_icon_round.png b/core/base/src/main/res/mipmap-xxhdpi/ic_launcher_icon_round.png
deleted file mode 100644
index 2d86de89..00000000
Binary files a/core/base/src/main/res/mipmap-xxhdpi/ic_launcher_icon_round.png and /dev/null differ
diff --git a/core/base/src/main/res/mipmap-xxxhdpi/ic_launcher_icon_round.png b/core/base/src/main/res/mipmap-xxxhdpi/ic_launcher_icon_round.png
deleted file mode 100644
index 16dc1a5e..00000000
Binary files a/core/base/src/main/res/mipmap-xxxhdpi/ic_launcher_icon_round.png and /dev/null differ
diff --git a/core/base/src/main/res/values-de/strings.xml b/core/base/src/main/res/values-de/strings.xml
index 401f5d77..8d0a90d6 100644
--- a/core/base/src/main/res/values-de/strings.xml
+++ b/core/base/src/main/res/values-de/strings.xml
@@ -377,7 +377,7 @@
aPa
Femtopascal
fPa
- Picopascal
+ Pikopascal
pPa
Nanopascal
nPa
@@ -421,7 +421,7 @@
am/s^2
Femtometer pro Quadratsekunde
fm/s^2
- Picometer pro Quadratsekunde
+ Pikometer pro Quadratsekunde
pm/s^2
Nanometer pro Quadratsekunde
mm/s^2
@@ -659,7 +659,9 @@
Farbthemen
PrÀzision
Trennzeichen
- Ausgabeformate
+
+
+ Exponentielle Notation
Einheitengruppen
Falsche Wechselkurse
Hinweis
@@ -668,29 +670,29 @@
Datenschutz-Bestimmungen
Richtlinien von Drittanbietern
Diese App bewerten
+ Formatierung
ZusÀtzliches
Anzahl der Dezimalstellen
Umgerechnete Werte können eine höhere PrÀzision als eingestellt haben.
- 1 000 (Maximum)
+ %1$s (Maximum)
Gruppentrennzeichen
- Punkt (42.069,12)
- Komma (42,069.12)
- Leerzeichen (42Â 069.12)
+ Punkt
+ Komma
+
+
+ Leerzeichen
- Ergebnisformatierung
- Wissenschaftliche Notation sieht wie 1E-21 aus
- Standard
- Wissenschaftliche Notation erlauben
- Wissenschaftliche Notation erzwingen
+
+ Einen Teil der Nummer durch E ersetzen
App Aussehen
- Automatisch
+ Automatisch
Hell
Dunkel
Farbthema
@@ -726,4 +728,415 @@
Einheitengruppe umordnen
Einheitengruppe deaktivieren
Versionsname
+ Ăber Unitto
+ Mehr ĂŒber die App erfahren
+ Einheiten deaktivieren und anordnen
+ Cent
+ cent
+
+
+ Maxwell
+ Mx
+ Weber
+ Wb
+ Milliweber
+ mWb
+ Microweber
+ ÎŒWb
+ Kiloweber
+ kWb
+ Megaweber
+ MWb
+ Gigaweber
+ GWb
+ Flux
+ Quellcode anzeigen
+ Diese App ĂŒbersetzen
+ Dem POEditor-Projekt beitreten und helfen
+
+
+ BinÀr
+ base2
+ TernÀr
+ base3
+ QuartÀr
+ base4
+ QuinÀr
+ base5
+ SenÀr
+ base6
+ SeptenÀr
+ base7
+ Oktal
+ base8
+ NonÀr
+ base9
+ Dezimal
+ base10
+ Undezimal
+ base11
+ Duodezimal
+ base12
+ Tridezimal
+ base13
+ Tetradezimal
+ base14
+ Pentadezimal
+ base15
+ Hexadezimal
+ base16
+ Basis
+ Vibrationen
+ Haptisches Feedback fĂŒr Tastaturtasten
+ Millibar
+ mbar
+ Kilopascal
+ kPa
+ Mikrometer Quecksilber
+ ÎŒmHg
+
+
+
+ Epochkonverter
+
+
+ Taschenrechner
+
+
+ y
+ m
+ Nautische Meile
+ M
+ Startbildschirm
+ WĂ€hlen Sie, welcher Bildschirm beim Starten der App angezeigt wird
+
+
+ Einheitenumrechner
+ Leeren
+ Verlauf leeren
+ Alle AusdrĂŒcke aus dem Verlauf werden fĂŒr immer gelöscht. Diese Aktion kann nicht rĂŒckgĂ€ngig gemacht werden!
+ Kein Verlauf
+ MenĂŒ öffnen
+ Mikrogramm
+ ”g
+
+
+ Attofarad
+ aF
+ Statfarad
+ stF
+ Farad
+ F
+ Exafarad
+ EF
+ Pikofarad
+ pF
+ Nanofarad
+ nF
+ Microfarad
+ ”F
+ Millifarad
+ mF
+ Kilofarad
+ kF
+ Megafarad
+ MF
+ Gigafarad
+ GF
+ Petafarad
+ PF
+
+
+ Quetta
+ Q
+ Ronna
+ R
+ Yotta
+ Y
+ Zetta
+ Z
+ Exa
+ E
+ Peta
+ P
+ Tera
+ T
+ Giga
+ G
+ Mega
+ M
+ Kilo
+ k
+ Hekto
+ h
+ Deka
+ da
+ Basis
+ Basis
+ Dezi
+ d
+ Zenti
+ c
+ Milli
+ m
+ Micro
+ Ό
+ Nano
+ n
+ Piko
+ P
+ Femto
+ f
+ Atto
+ a
+ Zepto
+ z
+ Yokto
+ y
+ Ronto
+ r
+ Quekto
+ q
+
+
+ Newton
+ N
+ Kilonewton
+ kN
+
+
+ Gramm-Kraft
+ gf
+
+
+ Kilogramm-Kraft
+ kgf
+ Tonnen-Kraft
+ tf
+ Millinewton
+ mN
+ Attonewton
+ aN
+ Dyn
+ dyn
+ Joule/Meter
+ J/m
+ Joule/Zentimeter
+ J/cm
+ Kilopfund-Kraft
+ kipf
+ Pfund-Kraft
+ lbf
+ Unzen-Kraft
+ ozf
+ Pond
+ p
+
+
+ Kilopond
+ kp
+
+
+ Newtonmeter
+ N*m
+ Newtonzentimeter
+ N*cm
+ Newtonmillimeter
+ N*mm
+ Kilonewtonmeter
+ kN*m
+ Dynmeter
+ dyn*m
+
+
+ Dynzentimeter
+ dyn*cm
+
+
+ Dynmillimeter
+ dyn*mm
+
+
+ Kilogramm-Kraft-Meter
+ kgf*m
+
+
+ Kilogramm-Kraft-Zentimeter
+ kgf*cm
+
+
+ Kilogramm-Kraft-Millimeter
+ kgf*mm
+
+
+ Gramm-Kraft Meter
+ gf*m
+
+
+ Gramm-Kraft Zentimeter
+ gf*cm
+
+
+ Gramm-Kraft Millimeter
+ gf*mm
+
+
+ Unzen-Kraft-FuĂ
+ ozf*ft
+
+
+ Unze-Kraft-Zoll
+ ozf*in
+ Pfund-Kraft-FuĂ
+ lbf*ft
+
+
+ Pfund-Kraft-Zoll
+ lbf*in
+
+
+ Liter/Stunde
+ L/h
+ Liter/Minute
+ L/m
+ Liter/Sekunde
+ L/s
+ Milliliter/Stunde
+ mL/h
+ Milliliter/Minute
+ mL/m
+ Milliliter/Sekunde
+ mL/s
+ Kubikmeter/Stunde
+ m3/h
+ Kubikmeter/Minute
+ m3/m
+ Kubikmeter/Sekunde
+ m3/s
+ Kubikmillimeter/Stunde
+ mm3/h
+ Kubikmillimeter/Minute
+ mm3/m
+ Kubikmillimeter/Sekunde
+ mm3/s
+ KubikfuĂ/Stunde
+ ft3/h
+ KubikfuĂ/Minute
+ ft3/m
+ KubikfuĂ/Sekunde
+ ft3/s
+ Gallone/Stunde (U.S.)
+ gal/h
+ Gallone/Minute(U.S.)
+ gal/m
+ Gallone/Sekunde(U.S.)
+ gal/s
+ Gallone/Stunde (Imperial)
+ gal/h
+ Gallone/Minute(Imperial)
+ gal/m
+ Gallone/Sekunde(Imperial)
+ gal/s
+
+
+ Candela/Quadratmeter
+ cd/m^2
+ Candela/Quadratzentimeter
+ cd/cm^2
+ Candela/QuadratfuĂ
+ cd/ft^2
+ Candela/Quadratzoll
+ cd/in^2
+ Kilocandela/Quadratmeter
+ kcd
+ Stilb
+ sb
+ Lumen/Quadratmeter/Steradiant
+ lm/m^2/sr
+ Lumen/Quadratzentimeter/Steradian
+ lm/cm^2/sr
+ Lumen/QuadratfuĂ/Steradian
+ lm/ft^2/sr
+ Watt/Quadratzentimeter/Steradian
+ W/cm^2/sr
+ Nit
+ nt
+ Millinit
+ mnt
+ Lambert
+ L
+ Millilambert
+ mL
+ FuĂ-Lambert
+ fL
+ Apostilb
+ asb
+ Blondel
+ blondel
+ Bril
+ bril
+ Skot
+ sk
+ KapazitÀt
+ PrÀfix
+ Kraft
+ Drehmoment
+
+
+ Fluss
+ Leuchtdichte
+
+
+ Zeit formatieren
+ Beispiel: 130 Minuten als 2h 10m anzeigen
+ Einheitenlistensortierung
+ Einheitenreihenfolge Àndern
+
+
+ Benutzung
+ Alphabetisch
+
+
+ Skala (Abst.)
+
+
+ Skala (Aufst.)
+ Farbthema auswÀhlen
+ Farbschema
+ AusgewÀhlte Farbe
+ Stil auswÀhlen
+
+
+ Genauigkeit und Zahlendarstellung
+ Kann nicht durch 0 teilen
+ Datumsunterschied
+
+
+ Zeit auswÀhlen
+ Anfang
+ Ende
+ Unterschied
+
+
+ Jahre
+
+
+ Monate
+
+
+ Tage
+
+
+ Stunden
+
+
+ Minuten
+
+
+ Weiter
\ No newline at end of file
diff --git a/core/base/src/main/res/values-en-rGB/strings.xml b/core/base/src/main/res/values-en-rGB/strings.xml
index 835208c6..2f7c8206 100644
--- a/core/base/src/main/res/values-en-rGB/strings.xml
+++ b/core/base/src/main/res/values-en-rGB/strings.xml
@@ -228,11 +228,11 @@
EB/s
- Attoliter
+ Attolitre
aL
- Milliliter
+ Millilitre
mL
- Liter
+ Litre
L
US liquid gallon
gal (US)
@@ -659,10 +659,8 @@
Themes
Precision
Separator
- Output format
+ Exponential notation
Unit groups
- Vibrations
- Haptic feedback when clicking keyboard buttons
Wrong currency rates?
Note
Currency rates are updated daily. There\'s no real-time market monitoring in the app
@@ -670,30 +668,26 @@
Privacy Policy
Third party licenses
Rate this app
- Formatting
+ Formatting
Additional
Number of decimal places
Converted values may have a precision higher than the preferred one.
- 1 000 (Max)
+ %1$s (Max)
Group separator symbol
- Period (42.069,12)
- Comma (42,069.12)
- Spaces (42Â 069.12)
+ Period
+ Comma
+ Space
- Result value formatting
- Engineering strings look like 1E-21
- Default
- Allow engineering
- Force engineering
+ Replace part of the number with E
App look and feel
- Auto
+ Auto
Light
Dark
Colour theme
@@ -787,4 +781,321 @@
Hexadecimal
base16
Base
+ Vibrations
+ Haptic feedback when clicking keyboard buttons
+ Millibar
+ mbar
+ Kilopascal
+ kPa
+ Micron of mercury
+ ÎŒmHg
+
+
+ Epoch converter
+
+
+ Calculator
+
+
+ y
+ m
+ Nautical mile
+ M
+ Starting screen
+ Choose which screen is shown when you launch the app
+
+
+ Unit converter
+ Clear
+ Clear history
+ All expressions from history will be deleted forever. This action can\'t be undone!
+ No history
+ Open menu
+ Microgram
+ ”g
+
+
+ Attofarad
+ aF
+ Statfarad
+ stF
+ Farad
+ F
+ Exafarad
+ EF
+ Picofarad
+ pF
+ Nanofarad
+ nF
+ Microfarad
+ ”F
+ Millifarad
+ mF
+ Kilofarad
+ kF
+ Megafarad
+ MF
+ Gigafarad
+ GF
+ Petafarad
+ PF
+
+
+ Quetta
+ Q
+ Ronna
+ R
+ Yotta
+ Y
+ Zetta
+ Z
+ Exa
+ E
+ Peta
+ P
+ Tera
+ T
+ Giga
+ G
+ Mega
+ M
+ Kilo
+ k
+ Hecto
+ h
+ Deca
+ da
+ Base
+ Base
+ Deci
+ d
+ Centi
+ c
+ Milli
+ m
+ Micro
+ Ό
+ Nano
+ n
+ Pico
+ p
+ Femto
+ f
+ Atto
+ a
+ Zepto
+ z
+ Yocto
+ y
+ Ronto
+ r
+ Quecto
+ q
+
+
+ Newton
+ N
+ Kilonewton
+ kN
+ Gram-force
+ gf
+ Kilogram-force
+ kgf
+ Ton-force
+ tf
+ Millinewton
+ mN
+ Attonewton
+ aN
+ Dyne
+ dyn
+ Joule/metre
+ J/m
+ Joule/centimetre
+ J/cm
+ Kilopound-force
+ kipf
+ Pound-force
+ lbf
+ Ounce-force
+ ozf
+ Pond
+ p
+ Kilopond
+ kp
+
+
+ Newton metre
+ N*m
+ Newton centimetre
+ N*cm
+ Newton millimetre
+ N*mm
+ Kilonewton metre
+ kN*m
+ Dyne metre
+ dyn*m
+ Dyne centimetre
+ dyn*cm
+ Dyne millimetre
+ dyn*mm
+ Kilogram-force metre
+ kgf*m
+ Kilogram-force centimetre
+ kgf*cm
+ Kilogram-force millimetre
+ kgf*mm
+ Gram-force metre
+ gf*m
+ Gram-force centimetre
+ gf*cm
+ Gram-force millimetre
+ gf*mm
+ Ounce-force foot
+ ozf*ft
+ Ounce-force inch
+ ozf*in
+ Pound-force foot
+ lbf*ft
+ Pound-force inch
+ lbf*in
+
+
+ Litre/hour
+ L/h
+ Litre/minute
+ L/m
+ Litre/second
+ L/s
+ Millilitre/hour
+ mL/h
+ Millilitre/minute
+ mL/m
+ Millilitre/second
+ mL/s
+ Cubic Metre/hour
+ m3/h
+ Cubic Metre/minute
+ m3/m
+ Cubic Metre/second
+ m3/s
+ Cubic Millimetre/hour
+ mm3/h
+ Cubic Millimetre/minute
+ mm3/m
+ Cubic Millimetre/second
+ mm3/s
+ Cubic Foot/hour
+ ft3/h
+ Cubic Foot/minute
+ ft3/m
+ Cubic Foot/second
+ ft3/s
+ Gallon/hour (U.S.)
+ gal/h
+ Gallon/minute (U.S.)
+ gal/m
+ Gallon/second (U.S.)
+ gal/s
+ Gallon/hour (Imperial)
+ gal/h
+ Gallon/minute (Imperial)
+ gal/m
+ Gallon/second (Imperial)
+ gal/s
+
+
+ Candela/square metre
+ cd/m^2
+ Candela/square centimetre
+ cd/cm^2
+ Candela/square foot
+ cd/ft^2
+ Candela/square inch
+ cd/in^2
+ Kilocandela/square metre
+ kcd
+ Stilb
+ sb
+ Lumen/square metre/steradian
+ lm/m^2/sr
+ Lumen/square centimetre/steradian
+ lm/cm^2/sr
+ Lumen/square foot/steradian
+ lm/ft^2/sr
+ Watt/square centimetre/steradian
+ W/cm^2/sr
+ Nit
+ nt
+ Millinit
+ mnt
+ Lambert
+ L
+ Millilambert
+ mL
+ Foot-lambert
+ fL
+ Apostilb
+ asb
+ Blondel
+ blondel
+ Bril
+ bril
+ Skot
+ sk
+ Capacitance
+ Prefix
+ Force
+ Torque
+ Flow
+ Luminance
+ Format time
+ Example: Show 130 minutes as 2h 10m
+ Units list sorting
+ Change units order
+
+
+ Usage
+ Alphabetical
+ Scale (Desc.)
+ Scale (Asc.)
+ Pick a theming mode
+ Colour scheme
+ Selected colour
+ Selected style
+
+
+ Precision and numbers appearance
+ Can\'t divide by 0
+ Date difference
+
+
+ Select time
+ Start
+ End
+ Difference
+
+
+ Years
+
+
+ Months
+
+
+ Days
+
+
+ Hours
+
+
+ Minutes
+
+
+ Next
+ Preview (click to switch)
\ No newline at end of file
diff --git a/core/base/src/main/res/values-fr/strings.xml b/core/base/src/main/res/values-fr/strings.xml
index 3b030f1f..c7009573 100644
--- a/core/base/src/main/res/values-fr/strings.xml
+++ b/core/base/src/main/res/values-fr/strings.xml
@@ -659,7 +659,9 @@
ThĂšmes
Précision
Séparateur
- Format de sortie
+
+
+ Notation exponentielle
Groupes d\'unités
Note
Les taux de change sont mis à jour quotidiennement. L\'application ne permet pas de suivre le marché en temps réel.
@@ -667,21 +669,22 @@
Politique de confidentialité
Licences tierces
Ăvaluer l\'application
- Formattage
+ Formattage
Additional
Nombre de décimales
Les valeurs converties peuvent avoir une précision supérieure à la précision préférée.
- 1 000 (Max)
+ %1$s (Max)
Symbole de séparation de groupe
- Période (42.069,12)
- Virgule (42,069.12)
- Espaces (42 069.12)
- Défaut
- Auto
+ Période
+ Virgule
+
+
+ Espaces
+ Auto
Clair
Sombre
AMOLED Noir
diff --git a/core/base/src/main/res/values-it/strings.xml b/core/base/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..cfe97c94
--- /dev/null
+++ b/core/base/src/main/res/values-it/strings.xml
@@ -0,0 +1,1101 @@
+
+
+
+
+ "Attometro"
+ "am"
+ "Nanometro"
+ "nm"
+ "Micrometro"
+ "ÎŒm"
+ "Millimetro"
+ "mm"
+ "Centimetro"
+ "cm"
+ "Decimetro"
+ "dm"
+ "Metro"
+ "m"
+ "Chilometro"
+ "km"
+ "Miglio"
+ "Mi"
+ "Iarda"
+ "yd"
+ "Piede"
+ "ft"
+ "Pollice"
+ "in"
+ "Anno luce"
+ "al"
+ "Parsec"
+ "pc"
+ "Kiloparsec"
+ "kpc"
+ "Megaparsec"
+ "Mpc"
+ "Raggio equatoriale di Mercurio"
+ "R Mercurio"
+ "Raggio equatoriale di Venere"
+ "R Venere"
+ "Raggio equatoriale della Terra"
+ "R Terra"
+ "Raggio equatoriale di Marte"
+ "R Marte"
+ "Raggio equatoriale di Giove"
+ "R Giove"
+ "Raggio equatoriale di Saturno"
+ "R Saturno"
+ "Raggio equatoriale di Urano"
+ "R Urano"
+ "Raggio equatoriale di Nettuno"
+ "R Nettuno"
+ "Raggio equatoriale del Sole"
+ "R Sole"
+
+
+ "Massa elettrone"
+ "m e"
+ "Dalton"
+ "u"
+ "Milligrammo"
+ "mg"
+ "Grammo"
+ "g"
+ "Chilogrammo"
+ "hg"
+ "Tonnellata metrica"
+ "t"
+ "Tonnellata imperiale"
+ "t (UK)"
+ "Libbra"
+ "lbs"
+ "Oncia"
+ "oz"
+ "Carato"
+ "ct"
+ "Massa di Mercurio"
+ "M Mercurio"
+ "Massa di Venere"
+ "M Venere"
+ "Massa della Terra"
+ "M Terra"
+ "Massa di Marte"
+ "M Marte"
+ "Massa di Giove"
+ "M Giove"
+ "Massa di Saturno"
+ "M Saturno"
+ "Massa di Urano"
+ "M Urano"
+ "Massa di Nettuno"
+ "M Nettuno"
+ "Massa del Sole"
+ "M Sole"
+
+
+ "Celsius"
+ "Fahrenheit"
+ "Kelvin"
+
+
+ "Millimetro all'ora"
+ "mm/h"
+ "Millimetro al minuto"
+ "mm/m"
+ "Millimetro al secondo"
+ "mm/s"
+ "Centimetro all'ora"
+ "cm/h"
+ "Centimetro al minuto"
+ "cm/m"
+ "Centimetro al secondo"
+ "cm/s"
+ "Metro all'ora"
+ "m/h"
+ "Metro al minuto"
+ "m/m"
+ "Metro al secondo"
+ "m/s"
+ "Chilometro all'ora"
+ "km/h"
+ "Chilometro al minuto"
+ "km/m"
+ "Chilometro al secondo"
+ "km/s"
+ "Piede all'ora"
+ "ft/h"
+ "Piede al minuto"
+ "ft/m"
+ "Piede al secondo"
+ "ft/s"
+ "Iarda all'ora"
+ "yd/h"
+ "Iarda al minuto"
+ "yd/m"
+ "Iarda al secondo"
+ "yd/s"
+ "Miglia all'ora"
+ "mi/h"
+ "Miglia al minuto"
+ "mi/m"
+ "Miglia al secondo"
+ "mi/s"
+ "Nodi"
+ "kt"
+ "VelocitĂ della luce nel vuoto"
+ "Prima velocitĂ cosmica"
+ "Seconda velocitĂ cosmica"
+ "Terza velocitĂ cosmica"
+ "VelocitĂ orbitale della Terra"
+ "Mach"
+ "Mach (SI)"
+
+
+ "Bit"
+ "b"
+ "Kibibit"
+ "Kib"
+ "Kilobit"
+ "Kb"
+ "Megabit"
+ "Mb"
+ "Mebibit"
+ "Mib"
+ "Gigabit"
+ "Gb"
+ "Terabit"
+ "Tb"
+ "Petabit"
+ "Pb"
+ "Exabit"
+ "Eb"
+ "Byte"
+ "B"
+ "Kibibyte"
+ "KiB"
+ "Kilobyte"
+ "KB"
+ "Megabyte"
+ "MB"
+ "Mebibyte"
+ "MiB"
+ "Gigabyte"
+ "GB"
+ "Terabyte"
+ "TB"
+ "Petabyte"
+ "PB"
+ "Exabyte"
+ "EB"
+
+
+ "Bit al secondo"
+ "b/s"
+ "Kibibit al secondo"
+ "Kib/s"
+ "Kilobit al secondo"
+ "Kb/s"
+ "Megabit al secondo"
+ "Mb/s"
+ "Mebibit al secondo"
+ "Mib/s"
+ "Gigabit al secondo"
+ "Gb/s"
+ "Terabit al secondo"
+ "Tb/s"
+ "Petabit al secondo"
+ "Pb/s"
+ "Exabit al secondo"
+ "Eb/s"
+ "Byte al secondo"
+ "B/s"
+ "Kibibyte al secondo"
+ "KiB/s"
+ "Kilobyte al secondo"
+ "KB/s"
+ "Megabyte al secondo"
+ "MB/s"
+ "Mebibyte al secondo"
+ "MiB/s"
+ "Gigabyte al secondo"
+ "GB/s"
+ "Terabyte al secondo"
+ "TB/s"
+ "Petabyte al secondo"
+ "PB/s"
+ "Exabyte al secondo"
+ "EB/s"
+
+
+ "Attolitro"
+ "aL"
+ "Millilitro"
+ "mL"
+ "Litro"
+ "L"
+ "Gallone USA"
+ "gal (US)"
+ "Quarto US"
+ "qt (US)"
+ "Pinta US"
+ "pt (US)"
+ "Coppa US"
+ "cup (US)"
+ "Oncia liquida US"
+ "fl oz (US)"
+ "Cucchiaio US"
+ "cucchiaio (US)"
+ "Cucchiaino US"
+ "cucchiaino (US)"
+ "Gallone imperiale"
+ "gal (UK)"
+ "Quarto imperiale"
+ "qt (UK)"
+ "Pinta imperiale"
+ "pt (UK)"
+ "Coppa imperiale"
+ "tazza (UK)"
+ "Oncia liquida imperiale"
+ "fl oz (UK)"
+ "Cucchiaio imperiale"
+ "Cucchiaino (UK)"
+ "Cucchiaino imperiale"
+ "Cucchiaino (UK)"
+ "Millimetro cubo"
+ "mm^3"
+ "Centimetro cubo"
+ "cm^3"
+ "Metro cubo"
+ "m^3"
+ "Chilometro cubo"
+ "km^3"
+
+
+ "Attosecondo"
+ "as"
+ "Nanosecondo"
+ "ns"
+ "Microsecondo"
+ "”s"
+ "Millisecondo"
+ "ms"
+ "Jiffy"
+ "j"
+ "Secondo"
+ "s"
+ "Minuto"
+ "m"
+ "Ora"
+ "h"
+ "Giorno"
+ "g"
+ "Settimana"
+ "s"
+
+
+ "Sezione d'urto dell'elettrone"
+ "ecs"
+ "Acro"
+ "ac"
+ "Ettaro"
+ "ha"
+ "Piede quadrato"
+ "ft^2"
+ "Miglio quadrato"
+ "mi^2"
+ "Iarda quadrata"
+ "yd^2"
+ "Pollice quadrato"
+ "in^2"
+ "Micrometro quadrato"
+ "”m^2"
+ "Millimetro quadrato"
+ "mm^2"
+ "Centimetro quadrato"
+ "cm^2"
+ "Decimetro quadrato"
+ "dm^2"
+ "Metro quadrato"
+ "m^2"
+ "Chilometro quadrato"
+ "km^2"
+
+
+ "Elettronvolt"
+ "eV"
+ "Attojoule"
+ "aJ"
+ "Cavallo potenza"
+ "hp"
+ "Joule"
+ "J"
+ "Kilojoule"
+ "kJ"
+ "Megajoule"
+ "MJ"
+ "Gigajoule"
+ "GJ"
+ "Tonnellata di TNT"
+ "t"
+ "Chilotone di TNT"
+ "kt"
+ "Megatone di TNT"
+ "Mt"
+ "Gigatone di TNT"
+ "Gt"
+ "Calorie (th)"
+ "cal"
+ "Kilocalorie (th)"
+ "kcal"
+
+
+ "Attowatt"
+ "aW"
+ "Watt"
+ "W"
+ "Kilowatt"
+ "kW"
+ "Megawatt"
+ "MW"
+ "Cavallo di potenza"
+ "hp"
+
+
+ "Secondo d'angolo"
+ "Minuto"
+ "Grado"
+ "Radiante"
+ "rad"
+ "Sestante"
+ "sxt"
+ "Giro"
+ "tr"
+
+
+ "Attopascal"
+ "aPa"
+ "Femtopascal"
+ "fPa"
+ "Picopascal"
+ "pPa"
+ "Nanopascal"
+ "nPa"
+ "Micropascal"
+ "”Pa"
+ "Millipascal"
+ "mPa"
+ "Centipascal"
+ "cPa"
+ "Decipascal"
+ "dPa"
+ "Pascal"
+ "Pa"
+ "Decapascal"
+ "daPa"
+ "Ettopascal"
+ "hPa"
+ "Bar"
+ "bar"
+ "Megapascal"
+ "MPa"
+ "Gigapascal"
+ "GPA"
+ "Terapascal"
+ "TPa"
+ "Petapascal"
+ "PPa"
+ "Esapascal"
+ "EPa"
+ "Libbra/pollice quadrato"
+ "Kilolibbra/pollice quadrato"
+ "Atmosfera standard"
+ "atm"
+ "Torr"
+ "torr"
+ "Millimetro di mercurio"
+ "mmHg"
+
+
+ "Attometro/secondo quadrato"
+ "am/s^2"
+ "Femtometro/secondo quadrato"
+ "fm/s^2"
+ "Picometro/secondo quadrato"
+ "pm/s^2"
+ "Nanometro/secondo quadrato"
+ "nm/s^2"
+ "Micrometro/secondo quadrato"
+ "”m/s^2"
+ "Millimetro/secondo quadrato"
+ "mm/s^2"
+ "Centimetro/secondo quadrato"
+ "cm/s^2"
+ "Decimetro/secondo quadrato"
+ "dm/s^2"
+ "Metro/secondo quadrato"
+ "m/s^2"
+ "Kilometro/secondo quadrato"
+ "km/s^2"
+ "Decametro/secondo quadrato"
+ "dam/s^2"
+ "Ettometro/secondo quadrato"
+ "hm/s^2"
+ "Gallone"
+ "Gal"
+ "GravitĂ superficie di Mercurio"
+ "Mercurio g"
+ "GravitĂ superficie di Venere"
+ "Venere g"
+ "GravitĂ superficie della Terra"
+ "Terra g"
+ "GravitĂ superficie di Marte"
+ "Marte g"
+ "GravitĂ superficie di Giove"
+ "Giove g"
+ "GravitĂ superficie di Saturno"
+ "Saturno g"
+ "GravitĂ superficie di Urano"
+ "Urano g"
+ "GravitĂ superficie di Nettuno"
+ "Nettuno g"
+ "GravitĂ superficie del Sole"
+ "Sole g"
+ "Cardano"
+ "Dirham Emirati Arabi"
+ "Afghani"
+ "Lek albanese"
+ "Dram armeno"
+ "Fiorino delle Antille Olandesi"
+ "Kwanza angolano"
+ "Peso argentino"
+ "Dollaro australiano"
+ "Fiorino di Aruba"
+ "Manat dell'Azerbaigian"
+ "Marco bosniaco-ungherese convertibile"
+ "Dollaro delle Barbados"
+ "Taka bengalese"
+ "Lev bulgaro"
+ "Dinaro del Bahrain"
+ "Franco del Burundi"
+ "Dollaro delle Bermuda"
+ "Dollaro del Brunei"
+ "Boliviano"
+ "Real brasiliano"
+ "Dollaro delle Bahamas"
+ "Ngultrum del Bhutan"
+ "Pula del Botswana"
+ "Rublo bielorusso"
+ "Rublo bielorusso"
+ "Dollaro belizeano"
+ "Dollaro canadese"
+ "Franco congolese"
+ "Franco svizzero"
+ "Chilean Unit of Account (UF)"
+ "Peso cileno"
+ "Yuan cinese"
+ "Peso colombiano"
+ "ColĂłn costaricano"
+ "Peso cubano"
+ "Peso cubano"
+ "Escudo di Capo Verde"
+ "Corona ceca"
+ "Dai"
+ "Franco del Gibuti"
+ "Corona danese"
+ "Peso dominicano"
+ "Dinaro algerino"
+ "Lira egiziana"
+ "Nafka eritra"
+ "Birr etiope"
+ "Euro"
+ "Dollaro delle Fiji"
+ "Sterlina delle Falkland"
+ "Sterlina"
+ "Lari georgese"
+ "Cedi ghanese"
+ "Sterlina di Gibilterra"
+ "Dalasi gambiano"
+ "Franco guineano"
+ "Quetzal del Guatemala"
+ "Dollaro guyanese"
+ "Dollaro di Hong Kong"
+ "Lempira di Honduras"
+ "Kuna croata"
+ "Gourde di Haiti"
+ "Forint ungherese"
+ "Rupia indonesiana"
+ "Shekel"
+ "Rupia indiana"
+ "Dinaro iracheno"
+ "Rial iraniano"
+ "Corona islandese"
+ "Sterlina di Jersey"
+ "Dollaro giamaicano"
+ "Dinaro giordano"
+ "Yen giapponese"
+ "Scellino keniota"
+ "Som del Kirghizistan"
+ "Riel cambogiano"
+ "Franco delle Isole Comore"
+ "Won nord coreano"
+ "Won sud coreano"
+ "Dinaro del Kuwait"
+ "Dollaro Isole Cayman"
+ "Tenge kazako"
+ "Kip di Laos"
+ "Lira libanese"
+ "Rupia srilankese"
+ "Dollaro liberiano"
+ "Loti del Lesoto"
+ "Lita lituana"
+ "Lat lettone"
+ "Dinaro libico"
+ "Dirham marocchino"
+ "Leu moldavo"
+ "Ariary malgascio"
+ "Dinaro macedone"
+ "Kyat"
+ "Tugrik"
+ "Pataca"
+ "Ouguiya mauritana"
+ "Rupia mauritana"
+ "Rufiyaa maldiviana"
+ "Kwacha malawiano"
+ "Peso messicano"
+ "Ringgit malese"
+ "Metical mozambicano"
+ "Dollaro namibiano"
+ "Naira nigeriana"
+ "CĂłrdoba nicaraguense"
+ "Corona norvegeese"
+ "Rupia nepalese"
+ "Dollaro neozelandese"
+ "Riyal dell'Oman"
+ "Balboa panamense"
+ "Sol"
+ "Kina papuana"
+ "Peso filippino"
+ "Rupia pakistana"
+ "ZĆoty polacco"
+ "GuaranĂ paraguaiano"
+ "Rial qatariota"
+ "Leu rumeno"
+ "Dinaro serbo"
+ "Rublo russo"
+ "Franco ruandese"
+ "Riyal saudita"
+ "Dollaro delle Salomone"
+ "Rupia delle Seychelles"
+ "Dinaro sudanese"
+ "Corona svedese"
+ "Dollaro di Singapore"
+ "Shiba Inu"
+ "Sterlina di Sant'Elena"
+ "Leone del Sierra Leone"
+ "Scellinio somalo"
+ "Dollaro del Suriname"
+ "Dobra di SĂŁo TomĂ© e PrĂncipe (pre-2018)"
+ "Colon salvadoregno"
+ "Lira siriana"
+ "Lilangeni"
+ "Baht tailandese"
+ "Theta"
+ "Somoni del Tagikistan"
+ "Manat turkmeno"
+ "Dinaro tunisino"
+ "Paʻanga tongano"
+ "Lira turca"
+ "Dollaro di Trinidad & Tobago"
+ "Dollaro taiwanese"
+ "Scellino della Tanzania"
+ "Hryvnia ucraino"
+ "Scellino ugandese"
+ "Universe"
+ "Dollaro statunitense"
+ "USD Coin"
+ "Peso uruguaiano"
+ "Som uzbeco"
+ "BolĂvar venezuelano"
+ "Dong vietnamita"
+ "Vatu di Vanuatu"
+ "Wrapped Bitcoin"
+ "Tala samoano"
+ "Franco CFA"
+ "Oncia d'argento"
+ "Dollaro dei Caraibi Orientali"
+ "Diritti speciali di prelievo"
+ "West African CFA franc"
+ "Franco CFP"
+ "Rial yemenita"
+ "Rand sudafricano"
+ "Kwacha"
+ "Kwacha zambiano"
+ "Dollaro dello Zimbabwe"
+
+
+ "Lunghezza"
+ "Tempo"
+ "Volume"
+ "Area"
+ "Temperatura"
+ "VelocitĂ "
+ "Massa"
+ "Data"
+ "Energia"
+ "Potenza"
+ "Angolo"
+ "Traferimenti dati"
+ "Pressione"
+ "Accelerazione"
+ "Valuta"
+
+
+ "Converti da"
+ "Converti in"
+ "Impostazioni"
+
+
+ "Temi"
+ "Precisione"
+ "Separatore"
+ "Notazione esponenziale"
+ "Gruppi di unitĂ "
+ "Tassi di cambio sbagliati?"
+ "Note"
+ "I tassi di cambio sono aggiornati quotidianamente. Non c'Ăš alcun monitoraggio in tempo reale del mercato nell'app"
+ "Termini e condizioni"
+ "Politica sulla riservatezza"
+ "Licenze terze parti"
+ "Valuta questa app"
+ "Formato"
+ "Aggiuntive"
+
+
+ "Numero di posti decimali"
+ "Valori convertiti potrebbero avere una precisione maggiore di quella preferita."
+ "%1$s (Max)"
+
+
+ "Simbolo separatore gruppo"
+ "Punto"
+ "Virgola"
+ "Spazio"
+
+
+ "Sostituisce parte del numero con E"
+
+
+ "Aspetto dell'app"
+ "Auto"
+ "Chiaro"
+ "Scuro"
+ "Colore tema"
+ "Scuro AMOLED"
+ "Usa sfondo nero per temi scuri"
+ "Colori dinamici"
+ "Usa colori dal tuo sfondo"
+
+
+ "CaricamentoâŠ"
+ "Errore"
+ "Copiato %1$s!"
+ "Cancella"
+ "Cerca unitĂ "
+ "Nessun risultato trovato"
+ "Apri impostazioni"
+ "Assicurati che non ci siano errori, prova filtri diversi o controlla i gruppi di unitĂ disabilitati."
+ "Ciao!"
+ "Abilitato"
+ "Disabilitato"
+
+
+ "Naviga verso l'alto"
+ "Filtri selezionati"
+ "Apri impostazioni"
+ "Inverti unitĂ "
+ "Pulsante cerca"
+ "Svuota input"
+ "Aggiungi o rimuovi unitĂ dai preferiti"
+ "Svuota risultato ricerca"
+ "Apri o chiudi menĂč a discesa"
+ "Abilita gruppo unitĂ "
+ "Riordina gruppo unitĂ "
+ "Disabilita gruppi di unitĂ "
+ "Nome versione"
+ "Riguardo a Unitto"
+ "Impara riguardo all'app"
+ "Disabilita e riordina unitĂ "
+ "Centesimo"
+ "cent"
+
+
+ "Maxwell"
+ "Mx"
+ "Weber"
+ "Wb"
+ "Milliweber"
+ "mWb"
+ "Microweber"
+ "ÎŒWb"
+ "Kiloweber"
+ "kWb"
+ "Megaweber"
+ "MWb"
+ "Gigaweber"
+ "GWb"
+ "Flusso"
+ "Vedi codice sorgente"
+ "Traduci questa app"
+ "Unisciti al progetto POEditor per aiutare"
+
+
+ "Binario"
+ "base2"
+ "Ternario"
+ "base3"
+ "Quaternario"
+ "base4"
+ "Quinario"
+ "base5"
+ "Senario"
+ "base6"
+ "Settenario"
+ "base7"
+ "Ottale"
+ "base8"
+ "Nonario"
+ "base9"
+ "Decimale"
+ "base10"
+ "Undecimale"
+ "base11"
+ "Duodecimale"
+ "base12"
+ "Tridecimale"
+ "base13"
+ "Tetradecimale"
+ "base14"
+ "Pentadecimale"
+ "base15"
+ "Esadecimale"
+ "base16"
+ "Base"
+ "Vibrazioni"
+ "Feedback tattile quando si fa clic sui pulsanti della tastiera"
+ "Millibar"
+ "mbar"
+ "Kilopascal"
+ "kPa"
+ "Micron di mercurio"
+ "ÎŒmHg"
+
+
+ "Convertitore Epoch"
+
+
+ "Calcolatrice"
+
+
+ "a"
+ "m"
+ "Miglio nautico"
+ "M"
+ "Schermata iniziale"
+ "Scegli quale schermata Ăš mostrata quando avvi l'app"
+
+
+ "Convertitore di unitĂ "
+ "Pulisci"
+ "Pulisci cronologia"
+ "Tutti risultati dalla cronologia saranno eliminati per sempre. Questa azione non puĂČ essere annullata!"
+ "Nessuna cronologia"
+ "Apri menĂč"
+ "Microgram"
+ "”g"
+
+
+ "Attofarad"
+ "aF"
+ "Statfarad"
+ "stF"
+ "Farad"
+ "F"
+ "Exafarad"
+ "EF"
+ "Picofarad"
+ "pF"
+ "Nanofarad"
+ "nF"
+ "Microfarad"
+ "”F"
+ "Millifarad"
+ "mF"
+ "Kilofarad"
+ "kF"
+ "Megafarad"
+ "MF"
+ "Gigafarad"
+ "GF"
+ "Petafarad"
+ "PF"
+
+
+ "Quetta"
+ "Q"
+ "Ronna"
+ "R"
+ "Yotta"
+ "Y"
+ "Zetta"
+ "Z"
+ "Esa"
+ "E"
+ "Peta"
+ "P"
+ "Tera"
+ "T"
+ "Giga"
+ "G"
+ "Mega"
+ "M"
+ "Kilo"
+ "k"
+ "Etto"
+ "h"
+ "Deci"
+ "da"
+ "Base"
+ "Base"
+ "Deci"
+ "d"
+ "Centi"
+ "c"
+ "Milli"
+ "m"
+ "Micro"
+ "Ό"
+ "Nano"
+ "n"
+ "Pico"
+ "p"
+ "Femto"
+ "f"
+ "Atto"
+ "a"
+ "Zepto"
+ "z"
+ "Yocto"
+ "y"
+ "Ronto"
+ "r"
+ "Quecto"
+ "q"
+
+
+ "Newton"
+ "N"
+ "Kilonewton"
+ "kN"
+ "Grammo-forza"
+ "gf"
+ "Kilogrammo-forza"
+ "kgf"
+ "Tonnellata-forza"
+ "tf"
+ "Millinewton"
+ "mN"
+ "Attonewton"
+ "aN"
+ "Dyne"
+ "dyn"
+ "Joule/metro"
+ "J/m"
+ "Joule/centimetro"
+ "J/cm"
+ "Kilolibbra-forza"
+ "kipf"
+ "Libbra-forza"
+ "lbf"
+ "Oncia-forza"
+ "ozf"
+ "Pond"
+ "p"
+ "Kilopond"
+ "kp"
+
+
+ "Newton metro"
+ "N*m"
+ "Newton centimetro"
+ "N*cm"
+ "Newton millimetro"
+ "N*mm"
+ "Kilonewton metro"
+ "kN*m"
+ "Dyne metro"
+ "dyn*m"
+ "Dyne centimetro"
+ "dyn*cm"
+ "Dyne millimetro"
+ "dyn*mm"
+ "Kilogrammo-forza metro"
+ "kgf*m"
+ "Kilogrammo-forza centimetro"
+ "kgf*cm"
+ "Kilogrammo-forza millimetro"
+ "kgf*mm"
+ "Grammo-forza metro"
+ "gf*m"
+ "Grammo-forza centimetro"
+ "gf*cm"
+ "Grammo-forza millimetro"
+ "gf*mm"
+ "Oncia-forza piede"
+ "ozf*ft"
+ "Oncia-forza pollice"
+ "ozf*in"
+ "Libbra-forza piede"
+ "lbf*ft"
+ "Libbra-forza pollice"
+ "lbf*in"
+
+
+ "Litro/ora"
+ "L/h"
+ "Litro/minuto"
+ "L/m"
+ "Litro/secondo"
+ "L/s"
+ "Millilitro/ora"
+ "mL/h"
+ "Millilitro/minuto"
+ "mL/m"
+ "Millilitro/secondo"
+ "mL/s"
+ "Metro cubo/ora"
+ "m3/h"
+ "Metro cubo/minuto"
+ "m3/m"
+ "Metro cubo/secondo"
+ "m3/s"
+ "Millimetro cubo/ora"
+ "mm3/h"
+ "Millimetro cubo/minuto"
+ "mm3/m"
+ "Millimetro cubo/secondo"
+ "mm3/s"
+ "Piede cubo/ora"
+ "ft3/h"
+ "Piede cubo/minuto"
+ "ft3/m"
+ "Piede cubo/secondo"
+ "ft3/s"
+ "Gallone/ora (U.S.)"
+ "gal/h"
+ "Gallone/minuto (U.S.)"
+ "gal/m"
+ "Gallone/secondo (U.S.)"
+ "gal/s"
+ "Gallone/ora (Imperiale)"
+ "gal/h"
+ "Gallone/minuto (Imperiale)"
+ "gal/m"
+ "Gallone/secondo (Imperiale)"
+ "gal/s"
+
+
+ "Candela/metro quadrato"
+ "cd/m^2"
+ "Candela/centimetro quadrato"
+ "cd/cm^2"
+ "Candela/piede quadrato"
+ "cd/ft^2"
+ "Candela/pollice quadrato"
+ "cd/in^2"
+ "Kilocandela/metro quadrato"
+ "kcd"
+ "Stilb"
+ "sb"
+ "Lumen/metro quadrato/steradian"
+ "lm/m^2/sr"
+ "Lumen/centimetro quadrato/steradian"
+ "lm/cm^2/sr"
+ "Lumen/piede quadrato/steradian"
+ "lm/ft^2/sr"
+ "Watt/centimetro quadrato/steradian"
+ "W/cm^2/sr"
+ "Nit"
+ "nt"
+ "Millinit"
+ "mnt"
+ "Lambert"
+ "L"
+ "Millilambert"
+ "mL"
+ "Piede-lambert"
+ "fL"
+ "Apostilb"
+ "asb"
+ "Blondel"
+ "blondel"
+ "Bril"
+ "bril"
+ "Skot"
+ "sk"
+ "CapacitĂ "
+ "Prefisso"
+ "Forza"
+ "Coppia"
+ "Flusso"
+ "Luminanza"
+ "Formato tempo"
+ "Esempio: Mostra 130 minuti come 2h 10m"
+ "Ordine lista unitĂ "
+ "Cambia ordine unitĂ "
+
+
+ "Utilizzo"
+ "Alfabetico"
+ "Scala (Decr.)"
+ "Scala (Asc.)"
+ "Scegli una modalitĂ tematica"
+ "Schema colore"
+ "Colore selezionato"
+ "Stile selezionato"
+
+
+ "Precisione e aspetto dei numeri"
+ "Non divisibile per 0"
+ "Differenza di data"
+
+
+ "Seleziona orario"
+ "Inizio"
+ "Fine"
+ "Differenza"
+
+
+ "Anni"
+
+
+ "Mesi"
+
+
+ "Giorni"
+
+
+ "Ore"
+
+
+ "Minuti"
+
+
+ "Avanti"
+ "Anteprima (tocca per cambiare)"
+
\ No newline at end of file
diff --git a/core/base/src/main/res/values-ru/strings.xml b/core/base/src/main/res/values-ru/strings.xml
index 384a77c4..ffed6130 100644
--- a/core/base/src/main/res/values-ru/strings.xml
+++ b/core/base/src/main/res/values-ru/strings.xml
@@ -659,7 +659,7 @@
ĐąĐ”ĐŒŃ
ĐąĐŸŃĐœĐŸŃŃŃ
РазЎДлОŃДлŃ
- Đ€ĐŸŃĐŒĐ°Ń ĐČŃĐČĐŸĐŽĐ°
+ ĐĐșŃĐżĐŸĐœĐ”ĐœŃОалŃĐœĐ°Ń ĐœĐŸŃаŃĐžŃ
ĐŃŃĐżĐżŃ ĐČДлОŃĐžĐœ
ĐДпŃаĐČОлŃĐœŃĐ” ĐșŃŃŃŃ ĐČалŃŃ?
ĐĐœĐžĐŒĐ°ĐœĐžĐ”
@@ -668,35 +668,31 @@
ĐĐŸĐ»ĐžŃĐžĐșа ĐșĐŸĐœŃĐžĐŽĐ”ĐœŃОалŃĐœĐŸŃŃĐž
ĐĐžŃĐ”ĐœĐ·ĐžĐž ŃŃĐ”ŃŃĐžŃ
лОŃ
ĐŃĐ”ĐœĐžŃŃ ĐżŃĐžĐ»ĐŸĐ¶Đ”ĐœĐžĐ”
- Đ€ĐŸŃĐŒĐ°ŃĐžŃĐŸĐČĐ°ĐœĐžĐ”
+ Đ€ĐŸŃĐŒĐ°ŃĐžŃĐŸĐČĐ°ĐœĐžĐ”
ĐĐŸĐżĐŸĐ»ĐœĐžŃДлŃĐœĐŸĐ”
ĐĐŸĐ»ĐžŃĐ”ŃŃĐČĐŸ ĐŽĐ”ŃŃŃĐžŃĐœŃŃ
Đ·ĐœĐ°ĐșĐŸĐČ
ĐĐ”ŃĐ”ĐČĐŸĐŽĐžĐŒŃĐ” Đ·ĐœĐ°ŃĐ”ĐœĐžŃ ĐŒĐŸĐłŃŃ ĐžĐŒĐ”ŃŃ ŃĐŸŃĐœĐŸŃŃŃ ĐČŃŃĐ” ĐżŃĐ”ĐŽĐżĐŸŃŃĐžŃДлŃĐœĐŸĐč.
- 1 000 (ĐаĐșŃĐžĐŒŃĐŒ)
+ %1$s (ĐаĐșŃĐžĐŒŃĐŒ)
ĐĄĐžĐŒĐČĐŸĐ» ŃазЎДлОŃДлŃ
- ĐąĐŸŃĐșа (42.069,12)
- ĐапŃŃĐ°Ń (42,069.12)
- ĐŃĐŸĐ±Đ”Đ» (42 069.12)
+ ĐąĐŸŃĐșа
+ ĐапŃŃаŃ
+ ĐŃĐŸĐ±Đ”Đ»
- Đ€ĐŸŃĐŒĐ°Ń ŃДзŃĐ»ŃŃаŃа пДŃĐ”ĐČĐŸĐŽĐ°
- ĐĐœĐ¶Đ”ĐœĐ”ŃĐœŃĐč ŃĐŸŃĐŒĐ°Ń ĐČŃглŃĐŽĐžŃ ĐșаĐș 1E-21
- ĐĐŸ ŃĐŒĐŸĐ»ŃĐ°ĐœĐžŃ
- РазŃĐ”ŃĐžŃŃ ĐžĐœĐ¶Đ”ĐœĐ”ŃĐœŃĐč
- ĐŃĐ”ĐžĐŒŃŃĐ”ŃŃĐČĐ”ĐœĐœĐŸ ĐžĐœĐ¶Đ”ĐœĐ”ŃĐœŃĐč
+ ĐĐ°ĐŒĐ”ĐœĐžŃĐ” ŃаŃŃŃ ŃĐžŃла ĐœĐ° E
ĐĐœĐ”ŃĐœĐžĐč ĐČОЎ ĐżŃĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃ
- ĐĐČŃĐŸĐŒĐ°ŃĐžŃĐ”ŃĐșаŃ
+ ĐĐČŃĐŸ
ĐĄĐČĐ”ŃлаŃ
- ĐąĐ”ĐŒĐœĐ°Ń
+ ĐąŃĐŒĐœĐ°Ń
ĐŠĐČĐ”ŃĐŸĐČĐ°Ń ŃĐ”ĐŒĐ°
- ĐąĐ”ĐŒĐœĐ°Ń AMOLED
- ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČаŃŃ ŃĐ”ŃĐœŃĐč ŃĐŸĐœ ĐČ ŃĐ”ĐŒĐœŃŃ
ŃĐ”ĐŒĐ°Ń
+ ЧŃŃĐœĐ°Ń AMOLED
+ ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČаŃŃ ŃŃŃĐœŃĐč ŃĐŸĐœ ĐČ ŃŃĐŒĐœĐŸĐč ŃĐ”ĐŒĐ”
ĐĐžĐœĐ°ĐŒĐžŃĐœŃĐ” ŃĐČĐ”Ńа
ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČаŃŃ ŃĐČĐ”Ńа ĐŸĐ±ĐŸĐ”ĐČ
@@ -1048,7 +1044,7 @@
ĐĄĐșĐŸŃ
ŃĐș
ĐĐŒĐșĐŸŃŃŃ
- ĐŃĐ”ŃĐžŃĐș
+ ĐŃĐ”ŃĐžĐșŃ
ХОла
ĐĐŸĐŒĐ”ĐœŃ
йДŃĐ”ĐœĐžĐ”
@@ -1067,4 +1063,39 @@
ĐŠĐČĐ”ŃĐŸĐČĐ°Ń ŃŃ
Đ”ĐŒĐ°
ĐŃбŃĐ°ĐœĐœŃĐč ŃĐČĐ”Ń
ĐŃбŃĐ°ĐœĐœŃĐč ŃŃОлŃ
+
+
+ ĐąĐŸŃĐœĐŸŃŃŃ Đž ĐżŃДЎŃŃаĐČĐ»Đ”ĐœĐžĐ” ŃĐžŃДл
+ ĐДлŃĐ·Ń ĐŽĐ”Đ»ĐžŃŃ ĐœĐ° 0
+ Đ Đ°Đ·ĐœĐžŃа ĐŒĐ”Đ¶ĐŽŃ ĐŽĐ°ŃĐ°ĐŒĐž
+
+
+ ĐŃбДŃĐžŃĐ” ĐČŃĐ”ĐŒŃ
+ ĐаŃĐ°Đ»ĐŸ
+ ĐĐŸĐœĐ”Ń
+ Đ Đ°Đ·ĐœĐžŃа
+
+
+ ĐĐ”Ń
+
+
+ ĐĐ”ŃŃŃĐ”ĐČ
+
+
+ ĐĐœĐ”Đč
+
+
+ ЧаŃĐŸĐČ
+
+
+ ĐĐžĐœŃŃ
+
+
+ ĐалДД
+ ĐŃДЎпŃĐŸŃĐŒĐŸŃŃ (ĐœĐ°Đ¶ĐŒĐžŃĐ” ĐŽĐ»Ń ĐżĐ”ŃĐ”ĐșĐ»ŃŃĐ”ĐœĐžŃ)
\ No newline at end of file
diff --git a/core/base/src/main/res/values/strings.xml b/core/base/src/main/res/values/strings.xml
index d3e870e1..2b454edd 100644
--- a/core/base/src/main/res/values/strings.xml
+++ b/core/base/src/main/res/values/strings.xml
@@ -1,1370 +1,1366 @@
-
-
+
- Unitto
+ "Unitto"
-
- Attometer
- am
- Nanometer
- nm
- Micrometer
- ÎŒm
- Millimeter
- mm
- Centimeter
- cm
- Decimeter
- dm
- Meter
- m
- Kilometer
- km
- Mile
- Mi
- Nautical mile
- M
- Yard
- yd
- Foot
- ft
- Inch
- in
- Light year
- ly
- Parsec
- pc
- Kiloparsec
- kpc
- Megaparsec
- Mpc
- Mercury equatorial radius
- Mercury R
- Venus equatorial radius
- Venus R
- Earth equatorial radius
- Earth R
- Mars equatorial radius
- Mars R
- Jupiter equatorial radius
- Jupiter R
- Saturn equatorial radius
- Saturn R
- Uranus equatorial radius
- Uranus R
- Neptune equatorial radius
- Neptune R
- Sun equatorial radius
- Sun R
+
+ "Attometer"
+ "am"
+ "Nanometer"
+ "nm"
+ "Micrometer"
+ "ÎŒm"
+ "Millimeter"
+ "mm"
+ "Centimeter"
+ "cm"
+ "Decimeter"
+ "dm"
+ "Meter"
+ "m"
+ "Kilometer"
+ "km"
+ "Mile"
+ "Mi"
+ "Yard"
+ "yd"
+ "Foot"
+ "ft"
+ "Inch"
+ "in"
+ "Light year"
+ "ly"
+ "Parsec"
+ "pc"
+ "Kiloparsec"
+ "kpc"
+ "Megaparsec"
+ "Mpc"
+ "Mercury equatorial radius"
+ "Mercury R"
+ "Venus equatorial radius"
+ "Venus R"
+ "Earth equatorial radius"
+ "Earth R"
+ "Mars equatorial radius"
+ "Mars R"
+ "Jupiter equatorial radius"
+ "Jupiter R"
+ "Saturn equatorial radius"
+ "Saturn R"
+ "Uranus equatorial radius"
+ "Uranus R"
+ "Neptune equatorial radius"
+ "Neptune R"
+ "Sun equatorial radius"
+ "Sun R"
-
- Electron mass
- me
- Dalton
- u
- Microgram
- ”g
- Milligram
- mg
- Gram
- g
- Kilogram
- kg
- Metric ton
- t
- Imperial ton
- t (UK)
- Pound
- lbs
- Ounce
- oz
- Carat
- ct
- Mercury mass
- Mercury M
- Venus mass
- Venus M
- Earth mass
- Earth M
- Mars mass
- Mars M
- Jupiter mass
- Jupiter M
- Saturn mass
- Saturn M
- Uranus mass
- Uranus M
- Neptune mass
- Neptune M
- Sun mass
- Sun M
+
+ "Electron mass"
+ "me"
+ "Dalton"
+ "u"
+ "Milligram"
+ "mg"
+ "Gram"
+ "g"
+ "Kilogram"
+ "kg"
+ "Metric ton"
+ "t"
+ "Imperial ton"
+ "t (UK)"
+ "Pound"
+ "lbs"
+ "Ounce"
+ "oz"
+ "Carat"
+ "ct"
+ "Mercury mass"
+ "Mercury M"
+ "Venus mass"
+ "Venus M"
+ "Earth mass"
+ "Earth M"
+ "Mars mass"
+ "Mars M"
+ "Jupiter mass"
+ "Jupiter M"
+ "Saturn mass"
+ "Saturn M"
+ "Uranus mass"
+ "Uranus M"
+ "Neptune mass"
+ "Neptune M"
+ "Sun mass"
+ "Sun M"
-
- Celsius
- °C
- Fahrenheit
- °F
- Kelvin
- K
+
+ "Celsius"
+ "°C"
+ "Fahrenheit"
+ "°F"
+ "Kelvin"
+ "K"
-
- Millimeter/hour
- mm/h
- Millimeter/minute
- mm/m
- Millimeter/second
- mm/s
- Centimeter/hour
- cm/h
- Centimeter/minute
- cm/m
- Centimeter/second
- cm/s
- Meter/hour
- m/h
- Meter/minute
- m/m
- Meter/second
- m/s
- Kilometer/hour
- km/h
- Kilometer/minute
- km/m
- Kilometer/second
- km/s
- Foot/hour
- ft/h
- Foot/minute
- ft/m
- Foot/second
- ft/s
- Yard/hour
- yd/h
- Yard/minute
- yd/m
- Yard/second
- yd/s
- Mile/hour
- mi/h
- Mile/minute
- mi/m
- Mile/second
- mi/s
- Knot
- kt
- Speed of light in vacuum
- c
- First Cosmic Velocity
- v1
- Second Cosmic Velocity
- v2
- Third Cosmic Velocity
- v3
- Earth\'s orbital speed
- ve
- Mach
- M
- Mach (SI)
- M
+
+ "Millimeter/hour"
+ "mm/h"
+ "Millimeter/minute"
+ "mm/m"
+ "Millimeter/second"
+ "mm/s"
+ "Centimeter/hour"
+ "cm/h"
+ "Centimeter/minute"
+ "cm/m"
+ "Centimeter/second"
+ "cm/s"
+ "Meter/hour"
+ "m/h"
+ "Meter/minute"
+ "m/m"
+ "Meter/second"
+ "m/s"
+ "Kilometer/hour"
+ "km/h"
+ "Kilometer/minute"
+ "km/m"
+ "Kilometer/second"
+ "km/s"
+ "Foot/hour"
+ "ft/h"
+ "Foot/minute"
+ "ft/m"
+ "Foot/second"
+ "ft/s"
+ "Yard/hour"
+ "yd/h"
+ "Yard/minute"
+ "yd/m"
+ "Yard/second"
+ "yd/s"
+ "Mile/hour"
+ "mi/h"
+ "Mile/minute"
+ "mi/m"
+ "Mile/second"
+ "mi/s"
+ "Knot"
+ "kt"
+ "Speed of light in vacuum"
+ "c"
+ "First Cosmic Velocity"
+ "v1"
+ "Second Cosmic Velocity"
+ "v2"
+ "Third Cosmic Velocity"
+ "v3"
+ "Earth's orbital speed"
+ "ve"
+ "Mach"
+ "M"
+ "Mach (SI)"
+ "M"
-
- Bit
- b
- Kibibit
- Kib
- Kilobit
- Kb
- Megabit
- Mb
- Mebibit
- Mib
- Gigabit
- Gb
- Terabit
- Tb
- Petabit
- Pb
- Exabit
- Eb
- Byte
- B
- Kibibyte
- KiB
- Kilobyte
- KB
- Megabyte
- MB
- Mebibyte
- MiB
- Gigabyte
- GB
- Terabyte
- TB
- Petabyte
- PB
- Exabyte
- EB
+
+ "Bit"
+ "b"
+ "Kibibit"
+ "Kib"
+ "Kilobit"
+ "Kb"
+ "Megabit"
+ "Mb"
+ "Mebibit"
+ "Mib"
+ "Gigabit"
+ "Gb"
+ "Terabit"
+ "Tb"
+ "Petabit"
+ "Pb"
+ "Exabit"
+ "Eb"
+ "Byte"
+ "B"
+ "Kibibyte"
+ "KiB"
+ "Kilobyte"
+ "KB"
+ "Megabyte"
+ "MB"
+ "Mebibyte"
+ "MiB"
+ "Gigabyte"
+ "GB"
+ "Terabyte"
+ "TB"
+ "Petabyte"
+ "PB"
+ "Exabyte"
+ "EB"
-
- Bit/second
- b/s
- Kibibit/second
- Kib/s
- Kilobit/second
- Kb/s
- Megabit/second
- Mb/s
- Mebibit/second
- Mib/s
- Gigabit/second
- Gb/s
- Terabit/second
- Tb/s
- Petabit/second
- Pb/s
- Exabit/second
- Eb/s
- Byte/second
- B/s
- Kibibyte/second
- KiB/s
- Kilobyte/second
- KB/s
- Megabyte/second
- MB/s
- Mebibyte/second
- MiB/s
- Gigabyte/second
- GB/s
- Terabyte/second
- TB/s
- Petabyte/second
- PB/s
- Exabyte/second
- EB/s
+
+ "Bit/second"
+ "b/s"
+ "Kibibit/second"
+ "Kib/s"
+ "Kilobit/second"
+ "Kb/s"
+ "Megabit/second"
+ "Mb/s"
+ "Mebibit/second"
+ "Mib/s"
+ "Gigabit/second"
+ "Gb/s"
+ "Terabit/second"
+ "Tb/s"
+ "Petabit/second"
+ "Pb/s"
+ "Exabit/second"
+ "Eb/s"
+ "Byte/second"
+ "B/s"
+ "Kibibyte/second"
+ "KiB/s"
+ "Kilobyte/second"
+ "KB/s"
+ "Megabyte/second"
+ "MB/s"
+ "Mebibyte/second"
+ "MiB/s"
+ "Gigabyte/second"
+ "GB/s"
+ "Terabyte/second"
+ "TB/s"
+ "Petabyte/second"
+ "PB/s"
+ "Exabyte/second"
+ "EB/s"
-
- Attoliter
- aL
- Milliliter
- mL
- Liter
- L
- US liquid gallon
- gal (US)
- US liquid quart
- qt (US)
- US liquid pint
- pt (US)
- US legal cup
- cup (US)
- US fluid ounce
- fl oz (US)
- US tablespoon
- tablespoon (US)
- US teaspoon
- teaspoon (US)
- Imperial gallon
- gal (UK)
- Imperial quart
- qt (UK)
- Imperial pint
- pt (UK)
- Imperial cup
- cup (UK)
- Imperial fluid ounce
- fl oz (UK)
- Imperial tablespoon
- tablespoon (UK)
- Imperial teaspoon
- teaspoon (UK)
- Cubic millimeter
- mm^3
- Cubic centimeter
- cm^3
- Cubic meter
- m^3
- Cubic kilometer
- km^3
+
+ "Attoliter"
+ "aL"
+ "Milliliter"
+ "mL"
+ "Liter"
+ "L"
+ "US liquid gallon"
+ "gal (US)"
+ "US liquid quart"
+ "qt (US)"
+ "US liquid pint"
+ "pt (US)"
+ "US legal cup"
+ "cup (US)"
+ "US fluid ounce"
+ "fl oz (US)"
+ "US tablespoon"
+ "tablespoon (US)"
+ "US teaspoon"
+ "teaspoon (US)"
+ "Imperial gallon"
+ "gal (UK)"
+ "Imperial quart"
+ "qt (UK)"
+ "Imperial pint"
+ "pt (UK)"
+ "Imperial cup"
+ "cup (UK)"
+ "Imperial fluid ounce"
+ "fl oz (UK)"
+ "Imperial tablespoon"
+ "tablespoon (UK)"
+ "Imperial teaspoon"
+ "teaspoon (UK)"
+ "Cubic millimeter"
+ "mm^3"
+ "Cubic centimeter"
+ "cm^3"
+ "Cubic meter"
+ "m^3"
+ "Cubic kilometer"
+ "km^3"
-
- Attosecond
- as
- Nanosecond
- ns
- Microsecond
- ”s
- Millisecond
- ms
- Jiffy
- j
- Second
- s
- Minute
- m
- Hour
- h
- Day
- d
- Week
- w
+
+ "Attosecond"
+ "as"
+ "Nanosecond"
+ "ns"
+ "Microsecond"
+ "”s"
+ "Millisecond"
+ "ms"
+ "Jiffy"
+ "j"
+ "Second"
+ "s"
+ "Minute"
+ "m"
+ "Hour"
+ "h"
+ "Day"
+ "d"
+ "Week"
+ "w"
-
- Electron cross section
- ecs
- Cent
- cent
- Acre
- ac
- Hectare
- ha
- Square foot
- ft^2
- Square mile
- mi^2
- Square yard
- yd^2
- Square inch
- in^2
- Square micrometer
- ”m^2
- Square millimeter
- mm^2
- Square centimeter
- cm^2
- Square decimeter
- dm^2
- Square meter
- m^2
- Square kilometer
- km^2
+
+ "Electron cross section"
+ "ecs"
+ "Acre"
+ "ac"
+ "Hectare"
+ "ha"
+ "Square foot"
+ "ft^2"
+ "Square mile"
+ "mi^2"
+ "Square yard"
+ "yd^2"
+ "Square inch"
+ "in^2"
+ "Square micrometer"
+ "”m^2"
+ "Square millimeter"
+ "mm^2"
+ "Square centimeter"
+ "cm^2"
+ "Square decimeter"
+ "dm^2"
+ "Square meter"
+ "m^2"
+ "Square kilometer"
+ "km^2"
-
- Electron volt
- eV
- Attojoule
- aJ
- Horse power
- hp
- Joule
- J
- Kilojoule
- kJ
- Megajoule
- MJ
- Gigajoule
- GJ
- Ton of TNT
- t
- Kiloton of TNT
- kt
- Megaton of TNT
- Mt
- Gigaton of TNT
- Gt
- Calorie (th)
- cal
- Kilocalorie (th)
- kcal
+
+ "Electron volt"
+ "eV"
+ "Attojoule"
+ "aJ"
+ "Horse power"
+ "hp"
+ "Joule"
+ "J"
+ "Kilojoule"
+ "kJ"
+ "Megajoule"
+ "MJ"
+ "Gigajoule"
+ "GJ"
+ "Ton of TNT"
+ "t"
+ "Kiloton of TNT"
+ "kt"
+ "Megaton of TNT"
+ "Mt"
+ "Gigaton of TNT"
+ "Gt"
+ "Calorie (th)"
+ "cal"
+ "Kilocalorie (th)"
+ "kcal"
-
- Attowatt
- aW
- Watt
- W
- Kilowatt
- kW
- Megawatt
- MG
- Horsepower
- hp
+
+ "Attowatt"
+ "aW"
+ "Watt"
+ "W"
+ "Kilowatt"
+ "kW"
+ "Megawatt"
+ "MG"
+ "Horsepower"
+ "hp"
-
- Second
- \"
- Minute
- \'
- Degree
- °
- Radian
- rad
- Sextant
- sxt
- Turn
- tr
+
+ "Second"
+ "\""
+ "Minute"
+ "'"
+ "Degree"
+ "°"
+ "Radian"
+ "rad"
+ "Sextant"
+ "sxt"
+ "Turn"
+ "tr"
-
- Attopascal
- aPa
- Femtopascal
- fPa
- Picopascal
- pPa
- Nanopascal
- nPa
- Micropascal
- ”Pa
- Millipascal
- mPa
- Centipascal
- cPa
- Decipascal
- dPa
- Pascal
- Pa
- Dekapascal
- daPa
- Hectopascal
- hPa
- Millibar
- mbar
- Bar
- bar
- Kilopascal
- kPa
- Megapascal
- MPa
- Gigapascal
- GPA
- Terapascal
- TPa
- Petapascal
- PPa
- Exapascal
- EPa
- Pound/square inch
- psi
- Kilopound/square inch
- ksi
- Standard atmosphere
- atm
- Torr
- torr
- Micron of mercury
- ÎŒmHg
- Millimeter of mercury
- mm Hg
+
+ "Attopascal"
+ "aPa"
+ "Femtopascal"
+ "fPa"
+ "Picopascal"
+ "pPa"
+ "Nanopascal"
+ "nPa"
+ "Micropascal"
+ "”Pa"
+ "Millipascal"
+ "mPa"
+ "Centipascal"
+ "cPa"
+ "Decipascal"
+ "dPa"
+ "Pascal"
+ "Pa"
+ "Dekapascal"
+ "daPa"
+ "Hectopascal"
+ "hPa"
+ "Bar"
+ "bar"
+ "Megapascal"
+ "MPa"
+ "Gigapascal"
+ "GPA"
+ "Terapascal"
+ "TPa"
+ "Petapascal"
+ "PPa"
+ "Exapascal"
+ "EPa"
+ "Pound/square inch"
+ "psi"
+ "Kilopound/square inch"
+ "ksi"
+ "Standard atmosphere"
+ "atm"
+ "Torr"
+ "torr"
+ "Millimeter of mercury"
+ "mm Hg"
-
- Attometer/square second
- am/s^2
- Femtometer/square second
- fm/s^2
- Picometer/square second
- pm/s^2
- Nanometer/square second
- nm/s^2
- Micrometer/square second
- ”m/s^2
- Millimeter/square second
- mm/s^2
- Centimeter/square second
- cm/s^2
- Decimeter/square second
- dm/s^2
- Meter/square second
- m/s^2
- Kilometer/square second
- km/s^2
- Dekameter/square second
- dam/s^2
- Hectometer/square second
- hm/s^2
- Gal
- Gal
- Mercury surface gravity
- Mercury g
- Venus surface gravity
- Venus g
- Earth surface gravity
- Earth g
- Mars surface gravity
- Mars g
- Jupiter surface gravity
- Jupiter g
- Saturn surface gravity
- Saturn g
- Uranus surface gravity
- Uranus g
- Neptune surface gravity
- Neptune g
- Sun surface gravity
- Sun g
+
+ "Attometer/square second"
+ "am/s^2"
+ "Femtometer/square second"
+ "fm/s^2"
+ "Picometer/square second"
+ "pm/s^2"
+ "Nanometer/square second"
+ "nm/s^2"
+ "Micrometer/square second"
+ "”m/s^2"
+ "Millimeter/square second"
+ "mm/s^2"
+ "Centimeter/square second"
+ "cm/s^2"
+ "Decimeter/square second"
+ "dm/s^2"
+ "Meter/square second"
+ "m/s^2"
+ "Kilometer/square second"
+ "km/s^2"
+ "Dekameter/square second"
+ "dam/s^2"
+ "Hectometer/square second"
+ "hm/s^2"
+ "Gal"
+ "Gal"
+ "Mercury surface gravity"
+ "Mercury g"
+ "Venus surface gravity"
+ "Venus g"
+ "Earth surface gravity"
+ "Earth g"
+ "Mars surface gravity"
+ "Mars g"
+ "Jupiter surface gravity"
+ "Jupiter g"
+ "Saturn surface gravity"
+ "Saturn g"
+ "Uranus surface gravity"
+ "Uranus g"
+ "Neptune surface gravity"
+ "Neptune g"
+ "Sun surface gravity"
+ "Sun g"
-
- 1inch Network
- NCH
- Cardano
- ADA
- United Arab Emirates Dirham
- AED
- Afghan afghani
- AFN
- Algorand
- LGO
- Albanian lek
- ALL
- Armenian dram
- AMD
- Netherlands Antillean Guilder
- ANG
- Angolan kwanza
- AOA
- Argentine peso
- ARS
- Atomic Coin
- TOM
- Australian dollar
- AUD
- Avalanche
- VAX
- Aruban florin
- AWG
- Azerbaijani manat
- AZN
- Bosnia-Herzegovina Convertible Mark
- BAM
- Bajan dollar
- BBD
- Bitcoin Cash
- BCH
- Bangladeshi taka
- BDT
- Bulgarian lev
- BGN
- Bahraini dinar
- BHD
- Burundian Franc
- BIF
- Bermudan dollar
- BMD
- Binance Coin
- BNB
- Brunei dollar
- BND
- Bolivian boliviano
- BOB
- Brazilian real
- BRL
- Bahamian dollar
- BSD
- Bitcoin
- BTC
- Bhutan currency
- BTN
- Binance USD
- USD
- Botswanan Pula
- BWP
- New Belarusian Ruble
- BYN
- Belarusian Ruble
- BYR
- Belize dollar
- BZD
- Canadian dollar
- CAD
- Congolese franc
- CDF
- Swiss franc
- CHF
- Chiliz
- CHZ
- Chilean Unit of Account (UF)
- CLF
- Chilean peso
- CLP
- Chinese Yuan
- CNY
- Colombian peso
- COP
- Costa Rican ColĂłn
- CRC
- Crypto.com Chain Token
- CRO
- Cuban convertible peso
- CUC
- Cuban Peso
- CUP
- Cape Verdean escudo
- CVE
- Czech koruna
- CZK
- Dai
- DAI
- Djiboutian franc
- DJF
- Danish krone
- DKK
- Dogecoin
- OGE
- Dominican peso
- DOP
- Dotcoin
- DOT
- Algerian dinar
- DZD
- Elrond
- GLD
- Egyptian pound
- EGP
- Enjin Coin
- ENJ
- Eritrean nakfa
- ERN
- Ethiopian birr
- ETB
- Ethereum Classic
- ETC
- Ether
- ETH
- Euro
- EUR
- FileCoin
- FIL
- Fijian dollar
- FJD
- Falkland Islands pound
- FKP
- FarmaTrust
- FTT
- Pound sterling
- GBP
- Georgian lari
- GEL
- GGPro
- GGP
- Ghanaian cedi
- GHS
- Gibraltar pound
- GIP
- Gambian dalasi
- GMD
- Guinean franc
- GNF
- Golden Ratio Token
- GRT
- Guatemalan quetzal
- GTQ
- Guyanaese Dollar
- GYD
- Hong Kong dollar
- HKD
- Honduran lempira
- HNL
- Croatian kuna
- HRK
- Haitian gourde
- HTG
- Hungarian forint
- HUF
- Internet Computer
- ICP
- Indonesian rupiah
- IDR
- Israeli New Shekel
- ILS
- CoinIMP
- IMP
- Injective
- INJ
- Indian rupee
- INR
- Iraqi dinar
- IQD
- Iranian rial
- IRR
- Icelandic krĂłna
- ISK
- Jersey Pound
- JEP
- Jamaican dollar
- JMD
- Jordanian dinar
- JOD
- Japanese yen
- JPY
- Kenyan shilling
- KES
- Kyrgystani Som
- KGS
- Cambodian riel
- KHR
- Comorian franc
- KMF
- North Korean won
- KPW
- South Korean won
- KRW
- Kusama
- KSM
- Kuwaiti dinar
- KWD
- Cayman Islands dollar
- KYD
- Kazakhstani tenge
- KZT
- Laotian Kip
- LAK
- Lebanese pound
- LBP
- ChainLink
- INK
- Sri Lankan rupee
- LKR
- Liberian dollar
- LRD
- Lesotho loti
- LSL
- Litecoin
- LTC
- Lithuanian litas
- LTL
- Luna Coin
- UNA
- Latvian lats
- LVL
- Libyan dinar
- LYD
- Moroccan dirham
- MAD
- Polygon
- TIC
- Moldovan leu
- MDL
- Malagasy ariary
- MGA
- Macedonian denar
- MKD
- Myanmar Kyat
- MMK
- Mongolian tugrik
- MNT
- Macanese pataca
- MOP
- Mauritanian ouguiya
- MRO
- Mauritian rupee
- MUR
- Maldivian rufiyaa
- MVR
- Malawian kwacha
- MWK
- Mexican peso
- MXN
- Malaysian ringgit
- MYR
- Mozambican Metical
- MZN
- Namibian dollar
- NAD
- Nigerian naira
- NGN
- Nicaraguan cĂłrdoba
- NIO
- Norwegian krone
- NOK
- Nepalese rupee
- NPR
- New Zealand dollar
- NZD
- Omani rial
- OMR
- Menlo One
- ONE
- Panamanian balboa
- PAB
- Sol
- PEN
- Papua New Guinean kina
- PGK
- Philippine peso
- PHP
- Pakistani rupee
- PKR
- Poland zĆoty
- PLN
- Paraguayan guarani
- PYG
- Qatari Rial
- QAR
- Romanian leu
- RON
- Serbian dinar
- RSD
- Russian ruble
- RUB
- Rwandan Franc
- RWF
- Saudi riyal
- SAR
- Solomon Islands dollar
- SBD
- Seychellois rupee
- SCR
- Sudanese pound
- SDG
- Swedish krona
- SEK
- Singapore dollar
- SGD
- Shiba Inu
- HIB
- Saint Helena pound
- SHP
- Sierra Leonean leone
- SLL
- Sola
- SOL
- Somali shilling
- SOS
- Surinamese dollar
- SRD
- SĂŁo TomĂ© and PrĂncipe Dobra (pre-2018)
- STD
- Salvadoran ColĂłn
- SVC
- Syrian pound
- SYP
- Swazi lilangeni
- SZL
- Thai baht
- THB
- Theta
- ETA
- Tajikistani somoni
- TJS
- Turkmenistani manat
- TMT
- Tunisian dinar
- TND
- Tongan Paʻanga
- TOP
- TRON
- TRX
- Turkish lira
- TRY
- Trinidad & Tobago Dollar
- TTD
- New Taiwan dollar
- TWD
- Tanzanian shilling
- TZS
- Ukrainian hryvnia
- UAH
- Ugandan shilling
- UGX
- Universe
- UNI
- United States dollar
- USD
- USD Coin
- SDC
- Tether
- SDT
- Uruguayan peso
- UYU
- Uzbekistani som
- UZS
- Sovereign Bolivar
- VEF
- Vechain
- VET
- Vietnamese dong
- VND
- Vanuatu vatu
- VUV
- Wrapped Bitcoin
- BTC
- Samoan tala
- WST
- Central African CFA franc
- XAF
- Silver Ounce
- XAG
- XauCoin
- XAU
- East Caribbean dollar
- XCD
- Special Drawing Rights
- XDR
- Stellar
- XLM
- Monero
- XMR
- West African CFA franc
- XOF
- CFP franc
- XPF
- XRP
- XRP
- Yemeni rial
- YER
- South African rand
- ZAR
- Zambian kwacha
- ZMK
- Zambian Kwacha
- ZMW
- Zimbabwean Dollar
- ZWL
+
+ "1inch Network"
+ "NCH"
+ "Cardano"
+ "ADA"
+ "United Arab Emirates Dirham"
+ "AED"
+ "Afghan afghani"
+ "AFN"
+ "Algorand"
+ "LGO"
+ "Albanian lek"
+ "ALL"
+ "Armenian dram"
+ "AMD"
+ "Netherlands Antillean Guilder"
+ "ANG"
+ "Angolan kwanza"
+ "AOA"
+ "Argentine peso"
+ "ARS"
+ "Atomic Coin"
+ "TOM"
+ "Australian dollar"
+ "AUD"
+ "Avalanche"
+ "VAX"
+ "Aruban florin"
+ "AWG"
+ "Azerbaijani manat"
+ "AZN"
+ "Bosnia-Herzegovina Convertible Mark"
+ "BAM"
+ "Bajan dollar"
+ "BBD"
+ "Bitcoin Cash"
+ "BCH"
+ "Bangladeshi taka"
+ "BDT"
+ "Bulgarian lev"
+ "BGN"
+ "Bahraini dinar"
+ "BHD"
+ "Burundian Franc"
+ "BIF"
+ "Bermudan dollar"
+ "BMD"
+ "Binance Coin"
+ "BNB"
+ "Brunei dollar"
+ "BND"
+ "Bolivian boliviano"
+ "BOB"
+ "Brazilian real"
+ "BRL"
+ "Bahamian dollar"
+ "BSD"
+ "Bitcoin"
+ "BTC"
+ "Bhutan currency"
+ "BTN"
+ "Binance USD"
+ "USD"
+ "Botswanan Pula"
+ "BWP"
+ "New Belarusian Ruble"
+ "BYN"
+ "Belarusian Ruble"
+ "BYR"
+ "Belize dollar"
+ "BZD"
+ "Canadian dollar"
+ "CAD"
+ "Congolese franc"
+ "CDF"
+ "Swiss franc"
+ "CHF"
+ "Chiliz"
+ "CHZ"
+ "Chilean Unit of Account (UF)"
+ "CLF"
+ "Chilean peso"
+ "CLP"
+ "Chinese Yuan"
+ "CNY"
+ "Colombian peso"
+ "COP"
+ "Costa Rican ColĂłn"
+ "CRC"
+ "Crypto.com Chain Token"
+ "CRO"
+ "Cuban convertible peso"
+ "CUC"
+ "Cuban Peso"
+ "CUP"
+ "Cape Verdean escudo"
+ "CVE"
+ "Czech koruna"
+ "CZK"
+ "Dai"
+ "DAI"
+ "Djiboutian franc"
+ "DJF"
+ "Danish krone"
+ "DKK"
+ "Dogecoin"
+ "OGE"
+ "Dominican peso"
+ "DOP"
+ "Dotcoin"
+ "DOT"
+ "Algerian dinar"
+ "DZD"
+ "Elrond"
+ "GLD"
+ "Egyptian pound"
+ "EGP"
+ "Enjin Coin"
+ "ENJ"
+ "Eritrean nakfa"
+ "ERN"
+ "Ethiopian birr"
+ "ETB"
+ "Ethereum Classic"
+ "ETC"
+ "Ether"
+ "ETH"
+ "Euro"
+ "EUR"
+ "FileCoin"
+ "FIL"
+ "Fijian dollar"
+ "FJD"
+ "Falkland Islands pound"
+ "FKP"
+ "FarmaTrust"
+ "FTT"
+ "Pound sterling"
+ "GBP"
+ "Georgian lari"
+ "GEL"
+ "GGPro"
+ "GGP"
+ "Ghanaian cedi"
+ "GHS"
+ "Gibraltar pound"
+ "GIP"
+ "Gambian dalasi"
+ "GMD"
+ "Guinean franc"
+ "GNF"
+ "Golden Ratio Token"
+ "GRT"
+ "Guatemalan quetzal"
+ "GTQ"
+ "Guyanaese Dollar"
+ "GYD"
+ "Hong Kong dollar"
+ "HKD"
+ "Honduran lempira"
+ "HNL"
+ "Croatian kuna"
+ "HRK"
+ "Haitian gourde"
+ "HTG"
+ "Hungarian forint"
+ "HUF"
+ "Internet Computer"
+ "ICP"
+ "Indonesian rupiah"
+ "IDR"
+ "Israeli New Shekel"
+ "ILS"
+ "CoinIMP"
+ "IMP"
+ "Injective"
+ "INJ"
+ "Indian rupee"
+ "INR"
+ "Iraqi dinar"
+ "IQD"
+ "Iranian rial"
+ "IRR"
+ "Icelandic krĂłna"
+ "ISK"
+ "Jersey Pound"
+ "JEP"
+ "Jamaican dollar"
+ "JMD"
+ "Jordanian dinar"
+ "JOD"
+ "Japanese yen"
+ "JPY"
+ "Kenyan shilling"
+ "KES"
+ "Kyrgystani Som"
+ "KGS"
+ "Cambodian riel"
+ "KHR"
+ "Comorian franc"
+ "KMF"
+ "North Korean won"
+ "KPW"
+ "South Korean won"
+ "KRW"
+ "Kusama"
+ "KSM"
+ "Kuwaiti dinar"
+ "KWD"
+ "Cayman Islands dollar"
+ "KYD"
+ "Kazakhstani tenge"
+ "KZT"
+ "Laotian Kip"
+ "LAK"
+ "Lebanese pound"
+ "LBP"
+ "ChainLink"
+ "INK"
+ "Sri Lankan rupee"
+ "LKR"
+ "Liberian dollar"
+ "LRD"
+ "Lesotho loti"
+ "LSL"
+ "Litecoin"
+ "LTC"
+ "Lithuanian litas"
+ "LTL"
+ "Luna Coin"
+ "UNA"
+ "Latvian lats"
+ "LVL"
+ "Libyan dinar"
+ "LYD"
+ "Moroccan dirham"
+ "MAD"
+ "Polygon"
+ "TIC"
+ "Moldovan leu"
+ "MDL"
+ "Malagasy ariary"
+ "MGA"
+ "Macedonian denar"
+ "MKD"
+ "Myanmar Kyat"
+ "MMK"
+ "Mongolian tugrik"
+ "MNT"
+ "Macanese pataca"
+ "MOP"
+ "Mauritanian ouguiya"
+ "MRO"
+ "Mauritian rupee"
+ "MUR"
+ "Maldivian rufiyaa"
+ "MVR"
+ "Malawian kwacha"
+ "MWK"
+ "Mexican peso"
+ "MXN"
+ "Malaysian ringgit"
+ "MYR"
+ "Mozambican Metical"
+ "MZN"
+ "Namibian dollar"
+ "NAD"
+ "Nigerian naira"
+ "NGN"
+ "Nicaraguan cĂłrdoba"
+ "NIO"
+ "Norwegian krone"
+ "NOK"
+ "Nepalese rupee"
+ "NPR"
+ "New Zealand dollar"
+ "NZD"
+ "Omani rial"
+ "OMR"
+ "Menlo One"
+ "ONE"
+ "Panamanian balboa"
+ "PAB"
+ "Sol"
+ "PEN"
+ "Papua New Guinean kina"
+ "PGK"
+ "Philippine peso"
+ "PHP"
+ "Pakistani rupee"
+ "PKR"
+ "Poland zĆoty"
+ "PLN"
+ "Paraguayan guarani"
+ "PYG"
+ "Qatari Rial"
+ "QAR"
+ "Romanian leu"
+ "RON"
+ "Serbian dinar"
+ "RSD"
+ "Russian ruble"
+ "RUB"
+ "Rwandan Franc"
+ "RWF"
+ "Saudi riyal"
+ "SAR"
+ "Solomon Islands dollar"
+ "SBD"
+ "Seychellois rupee"
+ "SCR"
+ "Sudanese pound"
+ "SDG"
+ "Swedish krona"
+ "SEK"
+ "Singapore dollar"
+ "SGD"
+ "Shiba Inu"
+ "HIB"
+ "Saint Helena pound"
+ "SHP"
+ "Sierra Leonean leone"
+ "SLL"
+ "Sola"
+ "SOL"
+ "Somali shilling"
+ "SOS"
+ "Surinamese dollar"
+ "SRD"
+ "SĂŁo TomĂ© and PrĂncipe Dobra (pre-2018)"
+ "STD"
+ "Salvadoran ColĂłn"
+ "SVC"
+ "Syrian pound"
+ "SYP"
+ "Swazi lilangeni"
+ "SZL"
+ "Thai baht"
+ "THB"
+ "Theta"
+ "ETA"
+ "Tajikistani somoni"
+ "TJS"
+ "Turkmenistani manat"
+ "TMT"
+ "Tunisian dinar"
+ "TND"
+ "Tongan Paʻanga"
+ "TOP"
+ "TRON"
+ "TRX"
+ "Turkish lira"
+ "TRY"
+ "Trinidad & Tobago Dollar"
+ "TTD"
+ "New Taiwan dollar"
+ "TWD"
+ "Tanzanian shilling"
+ "TZS"
+ "Ukrainian hryvnia"
+ "UAH"
+ "Ugandan shilling"
+ "UGX"
+ "Universe"
+ "UNI"
+ "United States dollar"
+ "USD"
+ "USD Coin"
+ "SDC"
+ "Tether"
+ "SDT"
+ "Uruguayan peso"
+ "UYU"
+ "Uzbekistani som"
+ "UZS"
+ "Sovereign Bolivar"
+ "VEF"
+ "Vechain"
+ "VET"
+ "Vietnamese dong"
+ "VND"
+ "Vanuatu vatu"
+ "VUV"
+ "Wrapped Bitcoin"
+ "BTC"
+ "Samoan tala"
+ "WST"
+ "Central African CFA franc"
+ "XAF"
+ "Silver Ounce"
+ "XAG"
+ "XauCoin"
+ "XAU"
+ "East Caribbean dollar"
+ "XCD"
+ "Special Drawing Rights"
+ "XDR"
+ "Stellar"
+ "XLM"
+ "Monero"
+ "XMR"
+ "West African CFA franc"
+ "XOF"
+ "CFP franc"
+ "XPF"
+ "XRP"
+ "XRP"
+ "Yemeni rial"
+ "YER"
+ "South African rand"
+ "ZAR"
+ "Zambian kwacha"
+ "ZMK"
+ "Zambian Kwacha"
+ "ZMW"
+ "Zimbabwean Dollar"
+ "ZWL"
-
- Maxwell
- Mx
- Weber
- Wb
- Milliweber
- mWb
- Microweber
- ÎŒWb
- Kiloweber
- kWb
- Megaweber
- MWb
- Gigaweber
- GWb
+
+ "Length"
+ "Time"
+ "Volume"
+ "Area"
+ "Temperature"
+ "Speed"
+ "Mass"
+ "Data"
+ "Energy"
+ "Power"
+ "Angle"
+ "Data transfer"
+ "Pressure"
+ "Acceleration"
+ "Currency"
-
- Binary
- base2
- Ternary
- base3
- Quaternary
- base4
- Quinary
- base5
- Senary
- base6
- Septenary
- base7
- Octal
- base8
- Nonary
- base9
- Decimal
- base10
- Undecimal
- base11
- Duodecimal
- base12
- Tridecimal
- base13
- Tetradecimal
- base14
- Pentadecimal
- base15
- Hexadecimal
- base16
+
+ "Convert from"
+ "Convert to"
+ "Settings"
-
- Attofarad
- aF
- Statfarad
- stF
- Farad
- F
- Exafarad
- EF
- Picofarad
- pF
- Nanofarad
- nF
- Microfarad
- ”F
- Millifarad
- mF
- Kilofarad
- kF
- Megafarad
- MF
- Gigafarad
- GF
- Petafarad
- PF
+
+ "Themes"
+ "Precision"
+ "Separator"
+ "Exponential notation"
+ "Unit groups"
+ "Wrong currency rates?"
+ "Note"
+ "Currency rates are updated daily. There's no real-time market monitoring in the app"
+ "Terms and Conditions"
+ "Privacy Policy"
+ "Third party licenses"
+ "Rate this app"
+ "Formatting"
+ "Additional"
-
- Quetta
- Q
- Ronna
- R
- Yotta
- Y
- Zetta
- Z
- Exa
- E
- Peta
- P
- Tera
- T
- Giga
- G
- Mega
- M
- Kilo
- k
- Hecto
- h
- Deca
- da
- Base
- Base
- Deci
- d
- Centi
- c
- Milli
- m
- Micro
- Ό
- Nano
- n
- Pico
- p
- Femto
- f
- Atto
- a
- Zepto
- z
- Yocto
- y
- Ronto
- r
- Quecto
- q
+
+ "Number of decimal places"
+ "Converted values may have a precision higher than the preferred one."
+ "%1$s (Max)"
-
- Newton
- N
- Kilonewton
- kN
- Gram-force
- gf
- Kilogram-force
- kgf
- Ton-force
- tf
- Millinewton
- mN
- Attonewton
- aN
- Dyne
- dyn
- Joule/meter
- J/m
- Joule/centimeter
- J/cm
- Kilopound-force
- kipf
- Pound-force
- lbf
- Ounce-force
- ozf
- Pond
- p
- Kilopond
- kp
+
+ "Group separator symbol"
+ "Period"
+ "Comma"
+ "Space"
-
- Newton meter
- N*m
- Newton centimeter
- N*cm
- Newton millimeter
- N*mm
- Kilonewton meter
- kN*m
- Dyne meter
- dyn*m
- Dyne centimeter
- dyn*cm
- Dyne millimeter
- dyn*mm
- Kilogram-force meter
- kgf*m
- Kilogram-force centimeter
- kgf*cm
- Kilogram-force millimeter
- kgf*mm
- Gram-force meter
- gf*m
- Gram-force centimeter
- gf*cm
- Gram-force millimeter
- gf*mm
- Ounce-force foot
- ozf*ft
- Ounce-force inch
- ozf*in
- Pound-force foot
- lbf*ft
- Pound-force inch
- lbf*in
+
+ "Replace part of the number with E"
-
- Liter/hour
- L/h
- Liter/minute
- L/m
- Liter/second
- L/s
- Milliliter/hour
- mL/h
- Milliliter/minute
- mL/m
- Milliliter/second
- mL/s
- Cubic Meter/hour
- m3/h
- Cubic Meter/minute
- m3/m
- Cubic Meter/second
- m3/s
- Cubic Millimeter/hour
- mm3/h
- Cubic Millimeter/minute
- mm3/m
- Cubic Millimeter/second
- mm3/s
- Cubic Foot/hour
- ft3/h
- Cubic Foot/minute
- ft3/m
- Cubic Foot/second
- ft3/s
- Gallon/hour (U.S.)
- gal/h
- Gallon/minute (U.S.)
- gal/m
- Gallon/second (U.S.)
- gal/s
- Gallon/hour (Imperial)
- gal/h
- Gallon/minute (Imperial)
- gal/m
- Gallon/second (Imperial)
- gal/s
+
+ "App look and feel"
+ "Auto"
+ "Light"
+ "Dark"
+ "Color theme"
+ "AMOLED Dark"
+ "Use black background for dark themes"
+ "Dynamic colors"
+ "Use colors from your wallpaper"
-
- Candela/square meter
- cd/m^2
- Candela/square centimeter
- cd/cm^2
- Candela/square foot
- cd/ft^2
- Candela/square inch
- cd/in^2
- Kilocandela/square meter
- kcd
- Stilb
- sb
- Lumen/square meter/steradian
- lm/m^2/sr
- Lumen/square centimeter/steradian
- lm/cm^2/sr
- Lumen/square foot/steradian
- lm/ft^2/sr
- Watt/square centimeter/steradian
- W/cm^2/sr
- Nit
- nt
- Millinit
- mnt
- Lambert
- L
- Millilambert
- mL
- Foot-lambert
- fL
- Apostilb
- asb
- Blondel
- blondel
- Bril
- bril
- Skot
- sk
+
+ "LoadingâŠ"
+ "Error"
+ "Click to try again"
+ "Copied %1$s!"
+ "Cancel"
+ "OK"
+ "Search units"
+ "No results found"
+ "Open settings"
+ "Make sure there are no typos, try different filters or check for disabled unit groups."
+ "Hello!"
+ "Enabled"
+ "Disabled"
-
- Length
- Time
- Volume
- Area
- Temperature
- Speed
- Mass
- Data
- Energy
- Power
- Angle
- Data transfer
- Pressure
- Acceleration
- Currency
- Flux
- Base
- Capacitance
- Prefix
- Force
- Torque
- Flow
- Luminance
+
+ "Navigate up"
+ "Checked filter"
+ "Open settings"
+ "Swap units"
+ "Search button"
+ "Clear input"
+ "Add or remove unit from favorites"
+ "Empty search result"
+ "Open or close drop down menu"
+ "Enable unit group"
+ "Reorder unit group"
+ "Disable unit group"
+ "Version name"
+ "About Unitto"
+ "Learn about the app"
+ "Disable and rearrange units"
+ "Cent"
+ "cent"
-
- Convert from
- Convert to
- Settings
+
+ "Maxwell"
+ "Mx"
+ "Weber"
+ "Wb"
+ "Milliweber"
+ "mWb"
+ "Microweber"
+ "ÎŒWb"
+ "Kiloweber"
+ "kWb"
+ "Megaweber"
+ "MWb"
+ "Gigaweber"
+ "GWb"
+ "Flux"
+ "View source code"
+ "Translate this app"
+ "Join POEditor project to help"
-
- Themes
- Precision
- Separator
- Output format
- Starting screen
- Choose which screen is shown when you launch the app
- Unit groups
- Vibrations
- Haptic feedback when clicking keyboard buttons
- Format time
- Example: Show 130 minutes as 2h 10m
- Units list sorting
- Change units order
- Wrong currency rates?
- Note
- Currency rates are updated daily. There\'s no real-time market monitoring in the app
- Terms and Conditions
- Privacy Policy
- View source code
- Translate this app
- Join POEditor project to help
- Third party licenses
- Rate this app
- Formatting
- Additional
+
+ "Binary"
+ "base2"
+ "Ternary"
+ "base3"
+ "Quaternary"
+ "base4"
+ "Quinary"
+ "base5"
+ "Senary"
+ "base6"
+ "Septenary"
+ "base7"
+ "Octal"
+ "base8"
+ "Nonary"
+ "base9"
+ "Decimal"
+ "base10"
+ "Undecimal"
+ "base11"
+ "Duodecimal"
+ "base12"
+ "Tridecimal"
+ "base13"
+ "Tetradecimal"
+ "base14"
+ "Pentadecimal"
+ "base15"
+ "Hexadecimal"
+ "base16"
+ "Base"
+ "Vibrations"
+ "Haptic feedback when clicking keyboard buttons"
+ "Millibar"
+ "mbar"
+ "Kilopascal"
+ "kPa"
+ "Micron of mercury"
+ "ÎŒmHg"
-
- Unit converter
- Epoch converter
+
+ "Epoch converter"
-
- Calculator
- Clear
- Clear history
- All expressions from history will be deleted forever. This action can\'t be undone!
- No history
+
+ "Calculator"
-
- Time zone
+
+ "y"
+ "m"
+ "Nautical mile"
+ "M"
+ "Starting screen"
+ "Choose which screen is shown when you launch the app"
-
- Number of decimal places
- Converted values may have a precision higher than the preferred one.
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1 000 (Max)
+
+ "Unit converter"
+ "Clear"
+ "Clear history"
+ "All expressions from history will be deleted forever. This action can't be undone!"
+ "No history"
+ "Open menu"
+ "Microgram"
+ "”g"
-
- Group separator symbol
- Period (42.069,12)
- Comma (42,069.12)
- Spaces (42Â 069.12)
+
+ "Attofarad"
+ "aF"
+ "Statfarad"
+ "stF"
+ "Farad"
+ "F"
+ "Exafarad"
+ "EF"
+ "Picofarad"
+ "pF"
+ "Nanofarad"
+ "nF"
+ "Microfarad"
+ "”F"
+ "Millifarad"
+ "mF"
+ "Kilofarad"
+ "kF"
+ "Megafarad"
+ "MF"
+ "Gigafarad"
+ "GF"
+ "Petafarad"
+ "PF"
-
- Result value formatting
- Engineering strings look like 1E-21
- Default
- Allow engineering
- Force engineering
+
+ "Quetta"
+ "Q"
+ "Ronna"
+ "R"
+ "Yotta"
+ "Y"
+ "Zetta"
+ "Z"
+ "Exa"
+ "E"
+ "Peta"
+ "P"
+ "Tera"
+ "T"
+ "Giga"
+ "G"
+ "Mega"
+ "M"
+ "Kilo"
+ "k"
+ "Hecto"
+ "h"
+ "Deca"
+ "da"
+ "Base"
+ "Base"
+ "Deci"
+ "d"
+ "Centi"
+ "c"
+ "Milli"
+ "m"
+ "Micro"
+ "Ό"
+ "Nano"
+ "n"
+ "Pico"
+ "p"
+ "Femto"
+ "f"
+ "Atto"
+ "a"
+ "Zepto"
+ "z"
+ "Yocto"
+ "y"
+ "Ronto"
+ "r"
+ "Quecto"
+ "q"
-
- Usage
- Alphabetical
- Scale (Desc.)
- Scale (Asc.)
+
+ "Newton"
+ "N"
+ "Kilonewton"
+ "kN"
+ "Gram-force"
+ "gf"
+ "Kilogram-force"
+ "kgf"
+ "Ton-force"
+ "tf"
+ "Millinewton"
+ "mN"
+ "Attonewton"
+ "aN"
+ "Dyne"
+ "dyn"
+ "Joule/meter"
+ "J/m"
+ "Joule/centimeter"
+ "J/cm"
+ "Kilopound-force"
+ "kipf"
+ "Pound-force"
+ "lbf"
+ "Ounce-force"
+ "ozf"
+ "Pond"
+ "p"
+ "Kilopond"
+ "kp"
-
- App look and feel
- Auto
- Light
- Dark
- Color theme
- Pick a theming mode
- AMOLED Dark
- Use black background for dark themes
- Dynamic colors
- Use colors from your wallpaper
- Color scheme
- Selected color
- Selected style
+
+ "Newton meter"
+ "N*m"
+ "Newton centimeter"
+ "N*cm"
+ "Newton millimeter"
+ "N*mm"
+ "Kilonewton meter"
+ "kN*m"
+ "Dyne meter"
+ "dyn*m"
+ "Dyne centimeter"
+ "dyn*cm"
+ "Dyne millimeter"
+ "dyn*mm"
+ "Kilogram-force meter"
+ "kgf*m"
+ "Kilogram-force centimeter"
+ "kgf*cm"
+ "Kilogram-force millimeter"
+ "kgf*mm"
+ "Gram-force meter"
+ "gf*m"
+ "Gram-force centimeter"
+ "gf*cm"
+ "Gram-force millimeter"
+ "gf*mm"
+ "Ounce-force foot"
+ "ozf*ft"
+ "Ounce-force inch"
+ "ozf*in"
+ "Pound-force foot"
+ "lbf*ft"
+ "Pound-force inch"
+ "lbf*in"
-
- LoadingâŠ
- Error
- Copied %1$s!
- Cancel
- OK
- Search units
- No results found
- Open settings
- Make sure there are no typos, try different filters or check for disabled unit groups.
- Hello!
- Enabled
- Disabled
+
+ "Liter/hour"
+ "L/h"
+ "Liter/minute"
+ "L/m"
+ "Liter/second"
+ "L/s"
+ "Milliliter/hour"
+ "mL/h"
+ "Milliliter/minute"
+ "mL/m"
+ "Milliliter/second"
+ "mL/s"
+ "Cubic Meter/hour"
+ "m3/h"
+ "Cubic Meter/minute"
+ "m3/m"
+ "Cubic Meter/second"
+ "m3/s"
+ "Cubic Millimeter/hour"
+ "mm3/h"
+ "Cubic Millimeter/minute"
+ "mm3/m"
+ "Cubic Millimeter/second"
+ "mm3/s"
+ "Cubic Foot/hour"
+ "ft3/h"
+ "Cubic Foot/minute"
+ "ft3/m"
+ "Cubic Foot/second"
+ "ft3/s"
+ "Gallon/hour (U.S.)"
+ "gal/h"
+ "Gallon/minute (U.S.)"
+ "gal/m"
+ "Gallon/second (U.S.)"
+ "gal/s"
+ "Gallon/hour (Imperial)"
+ "gal/h"
+ "Gallon/minute (Imperial)"
+ "gal/m"
+ "Gallon/second (Imperial)"
+ "gal/s"
-
- Navigate up
- Checked filter
- Open settings
- Swap units
- Search button
- Clear input
- Add or remove unit from favorites
- Empty search result
- Open or close drop down menu
- Enable unit group
- Reorder unit group
- Disable unit group
- Open menu
+
+ "Candela/square meter"
+ "cd/m^2"
+ "Candela/square centimeter"
+ "cd/cm^2"
+ "Candela/square foot"
+ "cd/ft^2"
+ "Candela/square inch"
+ "cd/in^2"
+ "Kilocandela/square meter"
+ "kcd"
+ "Stilb"
+ "sb"
+ "Lumen/square meter/steradian"
+ "lm/m^2/sr"
+ "Lumen/square centimeter/steradian"
+ "lm/cm^2/sr"
+ "Lumen/square foot/steradian"
+ "lm/ft^2/sr"
+ "Watt/square centimeter/steradian"
+ "W/cm^2/sr"
+ "Nit"
+ "nt"
+ "Millinit"
+ "mnt"
+ "Lambert"
+ "L"
+ "Millilambert"
+ "mL"
+ "Foot-lambert"
+ "fL"
+ "Apostilb"
+ "asb"
+ "Blondel"
+ "blondel"
+ "Bril"
+ "bril"
+ "Skot"
+ "sk"
+ "Capacitance"
+ "Prefix"
+ "Force"
+ "Torque"
+ "Flow"
+ "Luminance"
+ "Format time"
+ "Example: Show 130 minutes as 2h 10m"
+ "Units list sorting"
+ "Change units order"
- Version name
- About Unitto
- Learn about the app
- Disable and rearrange units
+
+ "Usage"
+ "Alphabetical"
+ "Scale (Desc.)"
+ "Scale (Asc.)"
+ "Pick a theming mode"
+ "Color scheme"
+ "Selected color"
+ "Selected style"
-
- y
- m
+
+ "Precision and numbers appearance"
+ "Can't divide by 0"
+ "Date difference"
+
+
+ "Select time"
+ "Start"
+ "End"
+ "Difference"
+
+
+ "Years"
+
+
+ "Months"
+
+
+ "Days"
+
+
+ "Hours"
+
+
+ "Minutes"
+
+
+ "Next"
+ "Preview (click to switch)"
\ No newline at end of file
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index e7cbbc37..51d74dca 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -16,6 +16,8 @@
* along with this program. If not, see .
*/
+@file:Suppress("UnstableApiUsage")
+
plugins {
id("unitto.library")
id("unitto.library.compose")
@@ -35,7 +37,6 @@ android {
dependencies {
testImplementation(libs.junit)
- testImplementation(libs.org.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/Formatter.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/Formatter.kt
index 4311461b..e69de29b 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/Formatter.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/Formatter.kt
@@ -1,251 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2022-2023 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.core.ui
-
-import android.content.Context
-import com.sadellie.unitto.core.base.Separator
-import com.sadellie.unitto.core.base.Token
-import java.math.BigDecimal
-import java.math.RoundingMode
-
-// Legacy, LOL. Will change later
-object Formatter : UnittoFormatter()
-
-open class UnittoFormatter {
- /**
- * This regex will catch things like "123.456", "123", ".456"
- */
- private val numbersRegex = Regex("[\\d.]+")
-
- private val SPACE = "Â "
- private val PERIOD = "."
- private val COMMA = ","
-
- /**
- * Grouping separator.
- */
- var grouping: String = SPACE
-
- /**
- * Fractional part separator.
- */
- var fractional = Token.comma
-
- private val timeDivisions by lazy {
- mapOf(
- R.string.day_short to BigDecimal("86400000000000000000000"),
- R.string.hour_short to BigDecimal("3600000000000000000000"),
- R.string.minute_short to BigDecimal("60000000000000000000"),
- R.string.second_short to BigDecimal("1000000000000000000"),
- R.string.millisecond_short to BigDecimal("1000000000000000"),
- R.string.microsecond_short to BigDecimal("1000000000000"),
- R.string.nanosecond_short to BigDecimal("1000000000"),
- R.string.attosecond_short to BigDecimal("1"),
- )
- }
-
- /**
- * 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) Token.comma else Token.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(Token.E)) return input.replace(Token.dot, fractional)
-
- var output = input
- val allNumbers: List = input.getOnlyNumbers()
-
- allNumbers.forEach {
- output = output.replace(it, formatNumber(it))
- }
-
- Token.internalToDisplay.forEach {
- output = output.replace(it.key, it.value)
- }
-
- return output
- }
-
- /**
- * Reapply formatting. Reverses [format] and applies [format] again.
- */
- fun reFormat(input: String): String {
- // We get 123.45,6789
- // We need 12.345,6789
-
- // 123.45,6789
- // Remove grouping
- // 12345,6789
- // Replace fractional with "." because formatter accepts only numbers where fractional is a dot
- return format(
- input
- .replace(grouping, "")
- .replace(fractional, Token.dot)
- )
- }
-
- /**
- * Helper method to change formatting from [input] with a specified [separator] to the one that
- * is set for this [UnittoFormatter].
- */
- fun fromSeparator(input: String, separator: Int): String {
- val sGrouping = when (separator) {
- Separator.PERIOD -> PERIOD
- Separator.COMMA -> COMMA
- else -> SPACE
- }
- .also { if (it == grouping) return input }
- val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
-
- return input
- .replace(sGrouping, "\t")
- .replace(sFractional, fractional)
- .replace("\t", grouping)
- }
-
- fun toSeparator(input: String, separator: Int): String {
- val output = filterUnknownSymbols(input).replace(fractional, Token.dot)
- val sGrouping = when (separator) {
- Separator.PERIOD -> PERIOD
- Separator.COMMA -> COMMA
- else -> SPACE
- }
- val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
-
- return format(output)
- .replace(grouping, "\t")
- .replace(fractional, sFractional)
- .replace("\t", sGrouping)
- }
-
- fun removeGrouping(input: String): String = input.replace(grouping, "")
-
- /**
- * Takes [input] and [basicUnit] of the unit to format it to be more human readable.
- *
- * @return String like "1d 12h 12s".
- */
- fun formatTime(context: Context, input: String, basicUnit: BigDecimal?): String {
- if (basicUnit == null) return Token._0
-
- try {
- // Don't need magic if the input is zero
- if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token._0
- } catch (e: NumberFormatException) {
- // For case such as "10-" and "("
- return Token._0
- }
- // Attoseconds don't need "magic"
- if (basicUnit.compareTo(BigDecimal.ONE) == 0) return formatNumber(input)
-
- var result = if (input.startsWith(Token.minus)) Token.minus else ""
- var remainingSeconds = BigDecimal(input)
- .abs()
- .multiply(basicUnit)
- .setScale(0, RoundingMode.HALF_EVEN)
-
- if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token._0
-
- timeDivisions.forEach { (timeStr, divider) ->
- val division = remainingSeconds.divideAndRemainder(divider)
- val time = division.component1()
- remainingSeconds = division.component2()
- if (time.compareTo(BigDecimal.ZERO) == 1) {
- result += "${formatNumber(time.toPlainString())}${context.getString(timeStr)} "
- }
- }
- return result.trimEnd()
- }
-
- /**
- * Format given [input].
- *
- * Input must be a number with dot!!!. Will replace grouping separators and fractional part (dot)
- * separators.
- *
- * @see grouping
- * @see fractional
- */
- private fun formatNumber(input: String): String {
- if (input.any { it.isLetter() }) return input
-
- var firstPart = input.takeWhile { it != '.' }
- val remainingPart = input.removePrefix(firstPart)
-
- // Number of empty symbols (spaces) we need to add to correctly split into chunks.
- val offset = 3 - firstPart.length.mod(3)
- val output = if (offset != 3) {
- // We add some spaces at the beginning so that last chunk has 3 symbols
- firstPart = " ".repeat(offset) + firstPart
- firstPart.chunked(3).joinToString(grouping).drop(offset)
- } else {
- firstPart.chunked(3).joinToString(grouping)
- }
-
- return (output + remainingPart.replace(".", fractional))
- }
-
- /**
- * @receiver Must be a string with a dot (".") used as a fractional.
- */
- private fun String.getOnlyNumbers(): List =
- numbersRegex.findAll(this).map(MatchResult::value).toList()
-
- fun filterUnknownSymbols(input: String): String {
- var clearStr = input.replace(" ", "")
- var garbage = clearStr
-
- // String with unknown symbols
- Token.knownSymbols.plus(fractional).forEach {
- garbage = garbage.replace(it, " ")
- }
-
- // Remove unknown symbols from input
- garbage.split(" ").forEach {
- clearStr = clearStr.replace(it, "")
- }
-
- clearStr = clearStr
- .replace(Token.divide, Token.divideDisplay)
- .replace(Token.multiply, Token.multiplyDisplay)
- .replace(Token.minus, Token.minusDisplay)
-
- return clearStr
- }
-}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/DateTimePickerDialog.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/DateTimePickerDialog.kt
new file mode 100644
index 00000000..3f76bd75
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/DateTimePickerDialog.kt
@@ -0,0 +1,284 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common
+
+import android.text.format.DateFormat
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.DatePicker
+import androidx.compose.material3.DatePickerDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TimePicker
+import androidx.compose.material3.TimePickerLayoutType
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.material3.rememberTimePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
+import com.sadellie.unitto.core.base.R
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.ZoneOffset
+import kotlin.math.max
+
+@Composable
+fun TimePickerDialog(
+ modifier: Modifier = Modifier,
+ localDateTime: LocalDateTime,
+ confirmLabel: String = stringResource(R.string.ok_label),
+ dismissLabel: String = stringResource(R.string.cancel_label),
+ onDismiss: () -> Unit = {},
+ onConfirm: (LocalDateTime) -> Unit,
+ vertical: Boolean
+) {
+ val pickerState = rememberTimePickerState(
+ localDateTime.hour,
+ localDateTime.minute,
+ DateFormat.is24HourFormat(LocalContext.current)
+ )
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ modifier = modifier.wrapContentHeight(),
+ properties = DialogProperties(usePlatformDefaultWidth = vertical)
+ ) {
+ Surface(
+ modifier = modifier,
+ shape = MaterialTheme.shapes.extraLarge,
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = 6.dp,
+ ) {
+ Column(
+ modifier = Modifier.padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.select_time),
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.align(Alignment.Start)
+ )
+
+ TimePicker(
+ state = pickerState,
+ modifier = Modifier.padding(top = 20.dp),
+ layoutType = if (vertical) TimePickerLayoutType.Vertical else TimePickerLayoutType.Horizontal
+ )
+
+ Row(
+ modifier = Modifier.align(Alignment.End),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ TextButton(
+ onClick = onDismiss
+ ) {
+ Text(text = dismissLabel)
+ }
+ TextButton(
+ onClick = {
+ onConfirm(
+ localDateTime
+ .withHour(pickerState.hour)
+ .withMinute(pickerState.minute)
+ )
+ }
+ ) {
+ Text(text = confirmLabel)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DatePickerDialog(
+ modifier: Modifier = Modifier,
+ localDateTime: LocalDateTime,
+ confirmLabel: String = stringResource(R.string.ok_label),
+ dismissLabel: String = stringResource(R.string.cancel_label),
+ onDismiss: () -> Unit = {},
+ onConfirm: (LocalDateTime) -> Unit,
+) {
+ val pickerState = rememberDatePickerState(localDateTime.toEpochSecond(ZoneOffset.UTC) * 1000)
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ modifier = modifier.wrapContentHeight(),
+ properties = DialogProperties(usePlatformDefaultWidth = false)
+ ) {
+ Surface(
+ modifier = modifier
+ .requiredWidth(360.dp)
+ .heightIn(max = 568.dp),
+ shape = DatePickerDefaults.shape,
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = 6.dp,
+ ) {
+ Column(verticalArrangement = Arrangement.SpaceBetween) {
+ DatePicker(state = pickerState)
+
+ Box(modifier = Modifier
+ .align(Alignment.End)
+ .padding(DialogButtonsPadding)) {
+
+ AlertDialogFlowRow(
+ mainAxisSpacing = DialogButtonsMainAxisSpacing,
+ crossAxisSpacing = DialogButtonsCrossAxisSpacing
+ ) {
+ TextButton(
+ onClick = onDismiss
+ ) {
+ Text(text = dismissLabel)
+ }
+ TextButton(
+ onClick = {
+ val millis = pickerState.selectedDateMillis ?: return@TextButton
+
+ val date = LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(millis), ZoneId.systemDefault()
+ )
+
+ onConfirm(
+ localDateTime
+ .withYear(date.year)
+ .withMonth(date.monthValue)
+ .withDayOfMonth(date.dayOfMonth)
+ )
+ }
+ ) {
+ Text(text = confirmLabel)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// From androidx/compose/material3/AlertDialog.kt
+@Composable
+private fun AlertDialogFlowRow(
+ mainAxisSpacing: Dp,
+ crossAxisSpacing: Dp,
+ content: @Composable () -> Unit
+) {
+ Layout(content) { measurables, constraints ->
+ val sequences = mutableListOf>()
+ val crossAxisSizes = mutableListOf()
+ val crossAxisPositions = mutableListOf()
+
+ var mainAxisSpace = 0
+ var crossAxisSpace = 0
+
+ val currentSequence = mutableListOf()
+ var currentMainAxisSize = 0
+ var currentCrossAxisSize = 0
+
+ // Return whether the placeable can be added to the current sequence.
+ fun canAddToCurrentSequence(placeable: Placeable) =
+ currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() +
+ placeable.width <= constraints.maxWidth
+
+ // Store current sequence information and start a new sequence.
+ fun startNewSequence() {
+ if (sequences.isNotEmpty()) {
+ crossAxisSpace += crossAxisSpacing.roundToPx()
+ }
+ sequences += currentSequence.toList()
+ crossAxisSizes += currentCrossAxisSize
+ crossAxisPositions += crossAxisSpace
+
+ crossAxisSpace += currentCrossAxisSize
+ mainAxisSpace = max(mainAxisSpace, currentMainAxisSize)
+
+ currentSequence.clear()
+ currentMainAxisSize = 0
+ currentCrossAxisSize = 0
+ }
+
+ for (measurable in measurables) {
+ // Ask the child for its preferred size.
+ val placeable = measurable.measure(constraints)
+
+ // Start a new sequence if there is not enough space.
+ if (!canAddToCurrentSequence(placeable)) startNewSequence()
+
+ // Add the child to the current sequence.
+ if (currentSequence.isNotEmpty()) {
+ currentMainAxisSize += mainAxisSpacing.roundToPx()
+ }
+ currentSequence.add(placeable)
+ currentMainAxisSize += placeable.width
+ currentCrossAxisSize = max(currentCrossAxisSize, placeable.height)
+ }
+
+ if (currentSequence.isNotEmpty()) startNewSequence()
+
+ val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth)
+
+ val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight)
+
+ layout(mainAxisLayoutSize, crossAxisLayoutSize) {
+ sequences.forEachIndexed { i, placeables ->
+ val childrenMainAxisSizes = IntArray(placeables.size) { j ->
+ placeables[j].width +
+ if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
+ }
+ val arrangement = Arrangement.End
+ val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
+ with(arrangement) {
+ arrange(
+ mainAxisLayoutSize, childrenMainAxisSizes,
+ layoutDirection, mainAxisPositions
+ )
+ }
+ placeables.forEachIndexed { j, placeable ->
+ placeable.place(
+ x = mainAxisPositions[j],
+ y = crossAxisPositions[i]
+ )
+ }
+ }
+ }
+ }
+}
+
+private val DialogButtonsPadding by lazy { PaddingValues(bottom = 8.dp, end = 6.dp) }
+private val DialogButtonsMainAxisSpacing by lazy { 8.dp }
+private val DialogButtonsCrossAxisSpacing by lazy { 12.dp }
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
index f91e66af..aa4493b4 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
@@ -20,25 +20,18 @@ package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration
import android.view.HapticFeedbackConstants
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.animateIntAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
+import kotlinx.coroutines.launch
@Composable
fun BasicKeyboardButton(
@@ -52,21 +45,21 @@ fun BasicKeyboardButton(
contentHeight: Float
) {
val view = LocalView.current
- val interactionSource = remember { MutableInteractionSource() }
- val isPressed by interactionSource.collectIsPressedAsState()
- val cornerRadius: Int by animateIntAsState(
- targetValue = if (isPressed) 30 else 50,
- animationSpec = tween(easing = FastOutSlowInEasing),
- )
+ val coroutineScope = rememberCoroutineScope()
UnittoButton(
modifier = modifier,
- onClick = onClick,
+ onClick = {
+ onClick()
+ if (allowVibration) {
+ coroutineScope.launch {
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+ }
+ }
+ },
onLongClick = onLongClick,
- shape = RoundedCornerShape(cornerRadius),
containerColor = containerColor,
- contentPadding = PaddingValues(),
- interactionSource = interactionSource
+ contentPadding = PaddingValues()
) {
Icon(
imageVector = icon,
@@ -75,10 +68,6 @@ fun BasicKeyboardButton(
tint = iconColor
)
}
-
- LaunchedEffect(key1 = isPressed) {
- if (isPressed and allowVibration) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
- }
}
@Composable
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/MenuButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/MenuButton.kt
index 2d9ce43e..7d1d1202 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/MenuButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/MenuButton.kt
@@ -24,7 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
/**
* Button that is used in Top bars
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/ModifierExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/ModifierExtensions.kt
new file mode 100644
index 00000000..6bcd4add
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/ModifierExtensions.kt
@@ -0,0 +1,87 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.Dp
+
+fun Modifier.squashable(
+ onClick: () -> Unit = {},
+ onLongClick: (() -> Unit)? = null,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource,
+ cornerRadiusRange: IntRange,
+ role: Role = Role.Button,
+) = composed {
+ val isPressed by interactionSource.collectIsPressedAsState()
+ val cornerRadius: Int by animateIntAsState(
+ targetValue = if (isPressed) cornerRadiusRange.first else cornerRadiusRange.last,
+ animationSpec = tween(easing = FastOutSlowInEasing),
+ )
+
+ Modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ interactionSource = interactionSource,
+ indication = rememberRipple(),
+ role = role,
+ enabled = enabled
+ )
+}
+
+fun Modifier.squashable(
+ onClick: () -> Unit = {},
+ onLongClick: (() -> Unit)? = null,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource,
+ cornerRadiusRange: ClosedRange,
+ role: Role = Role.Button,
+) = composed {
+ val isPressed by interactionSource.collectIsPressedAsState()
+ val cornerRadius: Dp by animateDpAsState(
+ targetValue = if (isPressed) cornerRadiusRange.start else cornerRadiusRange.endInclusive,
+ animationSpec = tween(easing = FastOutSlowInEasing),
+ )
+
+ Modifier
+ .clip(RoundedCornerShape(cornerRadius))
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ interactionSource = interactionSource,
+ indication = rememberRipple(),
+ role = role,
+ enabled = enabled
+ )
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/NavigateUpButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/NavigateUpButton.kt
index 38aea268..aa480982 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/NavigateUpButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/NavigateUpButton.kt
@@ -24,7 +24,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
/**
* Button that is used in Top bars
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SegmentedButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SegmentedButton.kt
index 79d19737..58195781 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SegmentedButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SegmentedButton.kt
@@ -66,7 +66,7 @@ fun RowScope.SegmentedButton(
label: String,
onClick: () -> Unit,
selected: Boolean,
- icon: ImageVector
+ icon: ImageVector? = null
) {
val containerColor =
if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface
@@ -81,14 +81,16 @@ fun RowScope.SegmentedButton(
),
contentPadding = PaddingValues(horizontal = 12.dp)
) {
- Crossfade(targetState = selected) {
- if (it) {
- Icon(Icons.Default.Check, null, Modifier.size(18.dp))
- } else {
- Icon(icon, null, Modifier.size(18.dp))
+ if (icon != null) {
+ Crossfade(targetState = selected) {
+ if (it) {
+ Icon(Icons.Default.Check, null, Modifier.size(18.dp))
+ } else {
+ Icon(icon, null, Modifier.size(18.dp))
+ }
}
+ Spacer(Modifier.width(8.dp))
}
- Spacer(Modifier.width(8.dp))
Text(label)
}
}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SettingsButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SettingsButton.kt
new file mode 100644
index 00000000..12253eae
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/SettingsButton.kt
@@ -0,0 +1,34 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.sadellie.unitto.core.base.R
+
+@Composable
+fun SettingsButton(onClick: () -> Unit) {
+ IconButton(onClick) {
+ Icon(Icons.Outlined.Settings, stringResource(R.string.open_settings_description))
+ }
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoButton.kt
index 76276f2f..5890bc65 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoButton.kt
@@ -19,7 +19,6 @@
package com.sadellie.unitto.core.ui.common
import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@@ -27,8 +26,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@@ -40,17 +37,14 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.semantics.Role
@Composable
fun UnittoButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
- shape: Shape = RoundedCornerShape(100),
+ enabled: Boolean = true,
containerColor: Color,
contentColor: Color = contentColorFor(containerColor),
border: BorderStroke? = null,
@@ -59,16 +53,16 @@ fun UnittoButton(
content: @Composable RowScope.() -> Unit
) {
Surface(
- modifier = modifier.clip(shape).combinedClickable(
+ modifier = modifier.squashable(
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
- indication = rememberRipple(),
- role = Role.Button,
+ cornerRadiusRange = 30..50,
+ enabled = enabled
),
color = containerColor,
contentColor = contentColor,
- border = border
+ border = border,
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoDrawerSheet.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoDrawerSheet.kt
index 30892e49..99f09e4c 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoDrawerSheet.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoDrawerSheet.kt
@@ -31,8 +31,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.TopLevelDestinations
-import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.ui.model.DrawerItems
@Composable
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoListItem.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoListItem.kt
index 6dcf5ae6..25e8d1d2 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoListItem.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoListItem.kt
@@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
/**
* Represents one item in list on Settings screen.
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoNavigationDrawer.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoNavigationDrawer.kt
new file mode 100644
index 00000000..64339077
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoNavigationDrawer.kt
@@ -0,0 +1,195 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common
+
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.anchoredDraggable
+import androidx.compose.foundation.gestures.animateTo
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.DrawerDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.model.DrawerItems
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+// Why do I have to do it myself?
+@Composable
+fun UnittoModalNavigationDrawer(
+ drawer: @Composable () -> Unit,
+ modifier: Modifier,
+ state: AnchoredDraggableState,
+ gesturesEnabled: Boolean,
+ scope: CoroutineScope,
+ content: @Composable () -> Unit,
+) {
+ Box(modifier.fillMaxSize()) {
+ content()
+
+ Scrim(
+ open = state.isOpen,
+ onClose = { if (gesturesEnabled) scope.launch { state.close() } },
+ fraction = {
+ fraction(state.anchors.minAnchor(), state.anchors.maxAnchor(), state.offset)
+ },
+ color = DrawerDefaults.scrimColor
+ )
+
+ // Drawer
+ Box(Modifier
+ .offset {
+ IntOffset(
+ x = state
+ .requireOffset()
+ .roundToInt(), y = 0
+ )
+ }
+ .anchoredDraggable(
+ state = state,
+ orientation = Orientation.Horizontal,
+ enabled = gesturesEnabled or state.isOpen,
+ )
+ .padding(end = 18.dp) // Draggable when closed
+ ) {
+ drawer()
+ }
+
+ }
+}
+
+@Composable
+private fun Scrim(
+ open: Boolean,
+ onClose: () -> Unit,
+ fraction: () -> Float,
+ color: Color,
+) {
+ val dismissDrawer = if (open) {
+ Modifier.pointerInput(onClose) { detectTapGestures { onClose() } }
+ } else {
+ Modifier
+ }
+
+ Canvas(
+ Modifier
+ .fillMaxSize()
+ .then(dismissDrawer)
+ ) {
+ drawRect(color, alpha = fraction())
+ }
+}
+
+enum class UnittoDrawerState { OPEN, CLOSED }
+
+@Composable
+fun rememberUnittoDrawerState(
+ initialValue: UnittoDrawerState = UnittoDrawerState.CLOSED,
+): AnchoredDraggableState {
+ val minValue = -with(LocalDensity.current) { 360.dp.toPx() }
+ val positionalThreshold = -minValue * 0.5f
+ val velocityThreshold = with(LocalDensity.current) { 400.dp.toPx() }
+
+ return remember {
+ AnchoredDraggableState(
+ initialValue = initialValue,
+ anchors = DraggableAnchors {
+ UnittoDrawerState.OPEN at 0F
+ UnittoDrawerState.CLOSED at minValue
+ },
+ positionalThreshold = { positionalThreshold },
+ velocityThreshold = { velocityThreshold },
+ animationSpec = tween()
+ )
+ }
+}
+
+val AnchoredDraggableState.isOpen
+ get() = this.currentValue == UnittoDrawerState.OPEN
+
+suspend fun AnchoredDraggableState.close() {
+ this.animateTo(UnittoDrawerState.CLOSED)
+}
+
+suspend fun AnchoredDraggableState.open() {
+ this.animateTo(UnittoDrawerState.OPEN)
+}
+
+private fun fraction(a: Float, b: Float, pos: Float) =
+ ((pos - a) / (b - a)).coerceIn(0f, 1f)
+
+@Preview(backgroundColor = 0xFFC8F7D4, showBackground = true, showSystemUi = true)
+@Composable
+private fun PreviewUnittoModalNavigationDrawer() {
+ val drawerState = rememberUnittoDrawerState(initialValue = UnittoDrawerState.OPEN)
+ val corScope = rememberCoroutineScope()
+
+ UnittoModalNavigationDrawer(
+ drawer = {
+ UnittoDrawerSheet(
+ modifier = Modifier,
+ mainTabs = listOf(
+ DrawerItems.Calculator,
+ DrawerItems.Calculator,
+ DrawerItems.Calculator,
+ ),
+ additionalTabs = listOf(
+ DrawerItems.Calculator,
+ DrawerItems.Calculator,
+ DrawerItems.Calculator,
+ ),
+ currentDestination = DrawerItems.Calculator.destination,
+ onItemClick = {}
+ )
+ },
+ modifier = Modifier,
+ state = drawerState,
+ gesturesEnabled = true,
+ scope = corScope,
+ content = {
+ Column {
+ Text(text = "Content")
+ Button(
+ onClick = { corScope.launch { drawerState.open() } }
+ ) {
+ Text(text = "BUTTON")
+ }
+ }
+ }
+ )
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSlider.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSlider.kt
new file mode 100644
index 00000000..d4e18137
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSlider.kt
@@ -0,0 +1,158 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.clipRect
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.math.ceil
+import kotlin.math.roundToInt
+
+@Composable
+fun UnittoSlider(
+ modifier: Modifier = Modifier,
+ value: Float,
+ valueRange: ClosedFloatingPointRange,
+ onValueChange: (Float) -> Unit,
+ onValueChangeFinished: (Float) -> Unit = {}
+) {
+ val animated = animateFloatAsState(targetValue = value)
+
+ Slider(
+ value = animated.value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ valueRange = valueRange,
+ onValueChangeFinished = { onValueChangeFinished(animated.value) },
+ track = { sliderPosition -> SquigglyTrack(sliderPosition) },
+ steps = valueRange.endInclusive.roundToInt(),
+ )
+}
+
+@Composable
+private fun SquigglyTrack(
+ sliderState: SliderState,
+ eachWaveWidth: Float = 80f,
+ strokeWidth: Float = 15f,
+ filledColor: Color = MaterialTheme.colorScheme.primary,
+ unfilledColor: Color = MaterialTheme.colorScheme.surfaceVariant
+) {
+ val coroutineScope = rememberCoroutineScope()
+ var direct by remember { mutableFloatStateOf(0.72f) }
+ val animatedDirect = animateFloatAsState(direct, spring(stiffness = Spring.StiffnessLow))
+ val slider = sliderState.valueRange.endInclusive
+
+ LaunchedEffect(sliderState.valueRange.endInclusive) {
+ coroutineScope.launch {
+ delay(200L)
+ direct *= -1
+ }
+ }
+
+ Canvas(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(20.dp)
+ ) {
+ val width = size.width
+ val height = size.height
+
+ val path = Path().apply {
+ moveTo(
+ x = strokeWidth / 2,
+ y = height.times(0.5f)
+ )
+ val amount = ceil(width.div(eachWaveWidth))
+
+ repeat(amount.toInt()) {
+ val peek = if (it % 2 == 0) animatedDirect.value else -animatedDirect.value
+
+ relativeQuadraticBezierTo(
+ dx1 = eachWaveWidth * 0.5f,
+ // 0.75, because 1.0 was clipping out of bound for some reason
+ dy1 = height.times(peek),
+ dx2 = eachWaveWidth,
+ dy2 = 0f
+ )
+ }
+ }
+
+ clipRect(
+ top = 0f,
+ left = 0f,
+ right = width.times(slider),
+ bottom = height,
+ clipOp = ClipOp.Intersect
+ ) {
+ drawPath(
+ path = path,
+ color = filledColor,
+ style = Stroke(strokeWidth, cap = StrokeCap.Round)
+ )
+ }
+
+ drawLine(
+ color = unfilledColor,
+ start = Offset(width.times(slider), height.times(0.5f)),
+ end = Offset(width, height.times(0.5f)),
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round
+ )
+ }
+}
+
+@Preview(device = "spec:width=411dp,height=891dp")
+@Preview(device = "spec:width=673.5dp,height=841dp,dpi=480")
+@Preview(device = "spec:width=1280dp,height=800dp,dpi=480")
+@Preview(device = "spec:width=1920dp,height=1080dp,dpi=480")
+@Composable
+private fun PreviewNewSlider() {
+ var currentValue by remember { mutableFloatStateOf(0.9f) }
+
+ UnittoSlider(
+ value = currentValue,
+ valueRange = 0f..1f,
+ onValueChange = { currentValue = it }
+ )
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt
new file mode 100644
index 00000000..723cbb6b
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt
@@ -0,0 +1,73 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import com.sadellie.unitto.core.base.Token
+import kotlin.math.abs
+
+fun String.fixCursor(pos: Int, grouping: String): Int {
+
+ if (isEmpty()) return pos
+
+ // Best position if we move cursor left
+ var leftCursor = pos
+ while (this.isPlacedIllegallyAt(leftCursor, grouping)) leftCursor--
+
+ // Best position if we move cursor right
+ var rightCursor = pos
+ while (this.isPlacedIllegallyAt(rightCursor, grouping)) rightCursor++
+
+ return listOf(leftCursor, rightCursor).minBy { abs(it - pos) }
+}
+
+fun String.tokenLengthAhead(pos: Int): Int {
+ Token.Func.allWithOpeningBracket.forEach {
+ if (pos.isAfterToken(this, it)) return it.length
+ }
+
+ return 1
+}
+
+private fun String.isPlacedIllegallyAt(pos: Int, grouping: String): Boolean {
+ // For things like "123,|456" - this is illegal
+ if (pos.isAfterToken(this, grouping)) return true
+
+ // For things like "123,456+c|os(8)" - this is illegal
+ Token.Func.allWithOpeningBracket.forEach {
+ if (pos.isAtToken(this, it)) return true
+ }
+
+ return false
+}
+
+private fun Int.isAtToken(str: String, token: String): Boolean {
+ val checkBound = (token.length - 1).coerceAtLeast(1)
+ return str
+ .substring(
+ startIndex = (this - checkBound).coerceAtLeast(0),
+ endIndex = (this + checkBound).coerceAtMost(str.length)
+ )
+ .contains(token)
+}
+
+private fun Int.isAfterToken(str: String, token: String): Boolean {
+ return str
+ .substring((this - token.length).coerceAtLeast(0), this)
+ .contains(token)
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ExpressionTransformer.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ExpressionTransformer.kt
new file mode 100644
index 00000000..668772ce
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ExpressionTransformer.kt
@@ -0,0 +1,82 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+
+class ExpressionTransformer(private val formatterSymbols: FormatterSymbols) : VisualTransformation {
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val formatted = text.text.formatExpression(formatterSymbols)
+ return TransformedText(
+ text = AnnotatedString(formatted),
+ offsetMapping = ExpressionMapping(text.text, formatted)
+ )
+ }
+
+ inner class ExpressionMapping(
+ private val original: String,
+ private val transformed: String
+ ) : OffsetMapping {
+ // Called when entering text (on each text change)
+ // Basically moves cursor to the right position
+ //
+ // original input is "1000" and cursor is placed at the end "1000|"
+ // the transformed is "1,000" where cursor should be? - "1,000|"
+ override fun originalToTransformed(offset: Int): Int {
+ if (offset <= 0) return 0
+ if (offset >= original.length) return transformed.length
+
+ val unformattedSubstr = original.take(offset)
+ var buffer = ""
+ var groupings = 0
+
+ run {
+ transformed.forEach {
+ when (it) {
+ formatterSymbols.grouping.first() -> groupings++
+ formatterSymbols.fractional.first() -> buffer += "."
+ else -> buffer += it
+ }
+ if (buffer == unformattedSubstr) return@run
+ }
+ }
+
+ return transformed.fixCursor(buffer.length + groupings, formatterSymbols.grouping)
+ }
+
+ // Called when clicking transformed text
+ // Snaps cursor to the right position
+ //
+ // the transformed is "1,000" and cursor is placed at the end "1,000|"
+ // original input is "1000" where cursor should be? - "1000|"
+ override fun transformedToOriginal(offset: Int): Int {
+ if (offset <= 0) return 0
+ if (offset >= transformed.length) return original.length
+
+ val grouping = formatterSymbols.grouping.first()
+ val fixedCursor = transformed.fixCursor(offset, formatterSymbols.grouping)
+ val addedSymbols = transformed.take(fixedCursor).count { it == grouping }
+ return fixedCursor - addedSymbols
+ }
+ }
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt
new file mode 100644
index 00000000..b9843698
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt
@@ -0,0 +1,193 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import android.content.Context
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.base.Token
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+private val numbersRegex by lazy { Regex("[\\d.]+") }
+
+private val timeDivisions by lazy {
+ mapOf(
+ R.string.day_short to BigDecimal("86400000000000000000000"),
+ R.string.hour_short to BigDecimal("3600000000000000000000"),
+ R.string.minute_short to BigDecimal("60000000000000000000"),
+ R.string.second_short to BigDecimal("1000000000000000000"),
+ R.string.millisecond_short to BigDecimal("1000000000000000"),
+ R.string.microsecond_short to BigDecimal("1000000000000"),
+ R.string.nanosecond_short to BigDecimal("1000000000"),
+ R.string.attosecond_short to BigDecimal("1"),
+ )
+}
+
+fun String.clearAndFilterExpression(formatterSymbols: FormatterSymbols): String {
+ var clean = this
+ .replace(formatterSymbols.grouping, "")
+ .replace(formatterSymbols.fractional, Token.Digit.dot)
+ .replace(" ", "")
+
+ Token.sexyToUgly.forEach { (token, ugliness) ->
+ ugliness.forEach {
+ clean = clean.replace(it, token)
+ }
+ }
+
+ return clean.cleanIt(Token.expressionTokens)
+}
+
+internal fun String.clearAndFilterNumberBase(): String {
+ return uppercase().cleanIt(Token.numberBaseTokens)
+}
+
+/**
+ * Format string time conversion result into a more readable format.
+ *
+ * @param basicUnit Basic unit of the unit we convert to
+ * @return String like "1d 12h 12s".
+ */
+fun String.formatTime(
+ context: Context,
+ basicUnit: BigDecimal?,
+ formatterSymbols: FormatterSymbols
+): String {
+ // We get ugly version of input (non-fancy minus)
+ val input = this
+
+ if (basicUnit == null) return Token.Digit._0
+
+ try {
+ // Don't need magic if the input is zero
+ if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token.Digit._0
+ } catch (e: NumberFormatException) {
+ // For case such as "10-" and "("
+ return Token.Digit._0
+ }
+ // Attoseconds don't need "magic"
+ if (basicUnit.compareTo(BigDecimal.ONE) == 0) return input.formatExpression(formatterSymbols)
+
+ var result = if (input.startsWith("-")) Token.Operator.minus else ""
+ var remainingSeconds = BigDecimal(input)
+ .abs()
+ .multiply(basicUnit)
+ .setScale(0, RoundingMode.HALF_EVEN)
+
+ if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token.Digit._0
+
+ timeDivisions.forEach { (timeStr, divider) ->
+ val division = remainingSeconds.divideAndRemainder(divider)
+ val time = division.component1()
+ remainingSeconds = division.component2()
+ if (time.compareTo(BigDecimal.ZERO) != 0) {
+ result += "${time.toPlainString().formatExpression(formatterSymbols)}${context.getString(timeStr)} "
+ }
+ }
+ return result.trimEnd()
+}
+
+fun String.formatExpression(
+ formatterSymbols: FormatterSymbols
+): String {
+ var input = this
+ // Don't do anything to engineering string.
+ if (input.contains(Token.DisplayOnly.engineeringE)) {
+ return input.replace(Token.Digit.dot, formatterSymbols.fractional)
+ }
+
+ numbersRegex
+ .findAll(input)
+ .map(MatchResult::value)
+ .forEach {
+ input = input.replace(it, it.formatNumber(formatterSymbols))
+ }
+
+ Token.sexyToUgly.forEach { (token, ugliness) ->
+ ugliness.forEach { uglySymbol ->
+ input = input.replace(uglySymbol, token)
+ }
+ }
+
+ return input
+}
+
+private fun String.formatNumber(
+ formatterSymbols: FormatterSymbols
+): String {
+ val input = this
+
+ if (input.any { it.isLetter() }) return input
+
+ var firstPart = input.takeWhile { it != '.' }
+ val remainingPart = input.removePrefix(firstPart)
+
+ // Number of empty symbols (spaces) we need to add to correctly split into chunks.
+ val offset = 3 - firstPart.length.mod(3)
+ val output = if (offset != 3) {
+ // We add some spaces at the beginning so that last chunk has 3 symbols
+ firstPart = " ".repeat(offset) + firstPart
+ firstPart.chunked(3).joinToString(formatterSymbols.grouping).drop(offset)
+ } else {
+ firstPart.chunked(3).joinToString(formatterSymbols.grouping)
+ }
+
+ return output.plus(remainingPart.replace(".", formatterSymbols.fractional))
+}
+
+private fun String.cleanIt(legalTokens: List): String {
+ val streamOfTokens = this
+
+ fun peekTokenAfter(cursor: Int): String? {
+ legalTokens.forEach { token ->
+ val subs = streamOfTokens
+ .substring(
+ cursor,
+ (cursor + token.length).coerceAtMost(streamOfTokens.length)
+ )
+ if (subs == token) {
+ // Got a digit, see if there are other digits coming after
+ if (token in Token.Digit.allWithDot) {
+ return streamOfTokens
+ .substring(cursor)
+ .takeWhile { Token.Digit.allWithDot.contains(it.toString()) }
+ }
+ return token
+ }
+ }
+ return null
+ }
+
+ var cursor = 0
+ var tokens = ""
+
+ while (cursor != streamOfTokens.length) {
+ val nextToken = peekTokenAfter(cursor)
+
+ if (nextToken != null) {
+ tokens += nextToken
+ cursor += nextToken.length
+ } else {
+ // Didn't find any token, move left slowly (by 1 symbol)
+ cursor++
+ }
+ }
+
+ return tokens
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterSymbols.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterSymbols.kt
new file mode 100644
index 00000000..de309dfc
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterSymbols.kt
@@ -0,0 +1,46 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import com.sadellie.unitto.core.base.Separator
+
+sealed class FormatterSymbols(val grouping: String, val fractional: String) {
+ object Spaces : FormatterSymbols("Â ", ".")
+ object Period : FormatterSymbols(".", ",")
+ object Comma : FormatterSymbols(",", ".")
+}
+
+object AllFormatterSymbols {
+ private val allFormatterSymbols by lazy {
+ hashMapOf(
+ Separator.SPACE to FormatterSymbols.Spaces,
+ Separator.PERIOD to FormatterSymbols.Period,
+ Separator.COMMA to FormatterSymbols.Comma
+ )
+ }
+
+ /**
+ * Defaults to [FormatterSymbols.Spaces] if not found.
+ *
+ * @see Separator
+ */
+ fun getById(separator: Int): FormatterSymbols {
+ return allFormatterSymbols.getOrElse(separator) { FormatterSymbols.Spaces }
+ }
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt
index 7ef702a6..e07348c5 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -54,105 +55,124 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
-import com.sadellie.unitto.core.base.Separator
-import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
import kotlin.math.ceil
import kotlin.math.roundToInt
@Composable
-fun InputTextField(
+fun ExpressionTextField(
modifier: Modifier,
value: TextFieldValue,
- textStyle: TextStyle = NumbersTextStyleDisplayLarge,
minRatio: Float = 1f,
- cutCallback: () -> Unit,
- pasteCallback: (String) -> Unit,
- onCursorChange: (IntRange) -> Unit,
+ cutCallback: () -> Unit = {},
+ pasteCallback: (String) -> Unit = {},
+ onCursorChange: (TextRange) -> Unit,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ formatterSymbols: FormatterSymbols,
+ readOnly: Boolean = false,
+ placeholder: String? = null,
) {
- val clipboardManager = LocalClipboardManager.current
- fun copyCallback() = clipboardManager.copyWithoutGrouping(value)
-
- val textToolbar = UnittoTextToolbar(
- view = LocalView.current,
- copyCallback = ::copyCallback,
- pasteCallback = {
- pasteCallback(
- Formatter.toSeparator(
- clipboardManager.getText()?.text ?: "", Separator.COMMA
- )
- )
- },
- cutCallback = {
- copyCallback()
- cutCallback()
- onCursorChange(value.selection.end..value.selection.end)
- }
- )
-
- CompositionLocalProvider(
- LocalTextInputService provides null,
- LocalTextToolbar provides textToolbar
- ) {
- AutoSizableTextField(
- modifier = modifier,
- value = value,
- textStyle = textStyle.copy(color = textColor),
- minRatio = minRatio,
- onValueChange = {
- onCursorChange(it.selection.start..it.selection.end)
- },
- showToolbar = textToolbar::showMenu,
- hideToolbar = textToolbar::hide
- )
- }
-}
-
-@Composable
-fun InputTextField(
- modifier: Modifier = Modifier,
- value: String,
- textStyle: TextStyle = NumbersTextStyleDisplayLarge,
- minRatio: Float = 1f,
- textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
-) {
- var textFieldValue by remember(value) {
- mutableStateOf(TextFieldValue(value, selection = TextRange(value.length)))
- }
val clipboardManager = LocalClipboardManager.current
fun copyCallback() {
- clipboardManager.copyWithoutGrouping(textFieldValue)
- textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.selection.end))
+ clipboardManager.copyWithoutGrouping(value, formatterSymbols)
+ onCursorChange(TextRange(value.selection.end))
}
- CompositionLocalProvider(
- LocalTextInputService provides null,
- LocalTextToolbar provides UnittoTextToolbar(
+ val textToolbar: UnittoTextToolbar = if (readOnly) {
+ UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
)
- ) {
- AutoSizableTextField(
- modifier = modifier,
- value = textFieldValue,
- onValueChange = { textFieldValue = it },
- textStyle = textStyle.copy(color = textColor),
- minRatio = minRatio,
- readOnly = true,
- interactionSource = interactionSource
+ } else {
+ UnittoTextToolbar(
+ view = LocalView.current,
+ copyCallback = ::copyCallback,
+ pasteCallback = {
+ pasteCallback(clipboardManager.getText()?.text?.clearAndFilterExpression(formatterSymbols) ?: "")
+ },
+ cutCallback = {
+ clipboardManager.copyWithoutGrouping(value, formatterSymbols)
+ cutCallback()
+ }
)
}
+
+ AutoSizableTextField(
+ modifier = modifier,
+ value = value,
+ formattedValue = value.text.formatExpression(formatterSymbols),
+ textStyle = NumbersTextStyleDisplayLarge.copy(color = textColor),
+ minRatio = minRatio,
+ onValueChange = { onCursorChange(it.selection) },
+ readOnly = readOnly,
+ showToolbar = textToolbar::showMenu,
+ hideToolbar = textToolbar::hide,
+ visualTransformation = ExpressionTransformer(formatterSymbols),
+ placeholder = placeholder,
+ textToolbar = textToolbar
+ )
+}
+
+@Composable
+fun UnformattedTextField(
+ modifier: Modifier,
+ value: TextFieldValue,
+ minRatio: Float = 1f,
+ cutCallback: () -> Unit = {},
+ pasteCallback: (String) -> Unit = {},
+ onCursorChange: (TextRange) -> Unit,
+ textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ readOnly: Boolean = false,
+ placeholder: String? = null,
+) {
+ val clipboardManager = LocalClipboardManager.current
+ fun copyCallback() {
+ clipboardManager.copy(value)
+ onCursorChange(TextRange(value.selection.end))
+ }
+
+ val textToolbar: UnittoTextToolbar = if (readOnly) {
+ UnittoTextToolbar(
+ view = LocalView.current,
+ copyCallback = ::copyCallback,
+ )
+ } else {
+ UnittoTextToolbar(
+ view = LocalView.current,
+ copyCallback = ::copyCallback,
+ pasteCallback = {
+ pasteCallback(clipboardManager.getText()?.text?.clearAndFilterNumberBase() ?: "")
+ },
+ cutCallback = {
+ clipboardManager.copy(value)
+ cutCallback()
+ }
+ )
+ }
+
+ AutoSizableTextField(
+ modifier = modifier,
+ value = value,
+ textStyle = NumbersTextStyleDisplayLarge.copy(color = textColor),
+ minRatio = minRatio,
+ onValueChange = { onCursorChange(it.selection) },
+ readOnly = readOnly,
+ showToolbar = textToolbar::showMenu,
+ hideToolbar = textToolbar::hide,
+ placeholder = placeholder,
+ textToolbar = textToolbar
+ )
}
@Composable
private fun AutoSizableTextField(
modifier: Modifier = Modifier,
value: TextFieldValue,
+ formattedValue: String = value.text,
textStyle: TextStyle = TextStyle(),
scaleFactor: Float = 0.95f,
minRatio: Float = 1f,
@@ -160,11 +180,14 @@ private fun AutoSizableTextField(
readOnly: Boolean = false,
showToolbar: (rect: Rect) -> Unit = {},
hideToolbar: () -> Unit = {},
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ placeholder: String? = null,
+ textToolbar: UnittoTextToolbar
) {
val focusRequester = remember { FocusRequester() }
val density = LocalDensity.current
+ val textValue = value.copy(value.text.take(2000))
var nFontSize: TextUnit by remember { mutableStateOf(0.sp) }
var minFontSize: TextUnit
@@ -174,14 +197,14 @@ private fun AutoSizableTextField(
) {
with(density) {
// Cursor handle is not visible without this, 0.836f is the minimum required factor here
- nFontSize = maxHeight.toSp() * 0.836f
+ nFontSize = maxHeight.toSp() * 0.835f
minFontSize = nFontSize * minRatio
}
// Modified: https://blog.canopas.com/autosizing-textfield-in-jetpack-compose-7a80f0270853
val calculateParagraph = @Composable {
Paragraph(
- text = value.text,
+ text = formattedValue,
style = textStyle.copy(fontSize = nFontSize),
constraints = Constraints(
maxWidth = ceil(with(density) { maxWidth.toPx() }).toInt()
@@ -210,45 +233,70 @@ private fun AutoSizableTextField(
)
var offset = Offset.Zero
- BasicTextField(
- value = value,
- onValueChange = {
- showToolbar(Rect(offset, 0f))
- hideToolbar()
- onValueChange(it)
- },
- modifier = Modifier
- .focusRequester(focusRequester)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null,
- onClick = {
- hideToolbar()
- focusRequester.requestFocus()
- onValueChange(value.copy(selection = TextRange.Zero))
- showToolbar(Rect(offset, 0f))
+ CompositionLocalProvider(
+ LocalTextInputService provides null,
+ LocalTextToolbar provides textToolbar
+ ) {
+ BasicTextField(
+ value = textValue,
+ onValueChange = {
+ showToolbar(Rect(offset, 0f))
+ hideToolbar()
+ onValueChange(it)
+ },
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = {
+ hideToolbar()
+ focusRequester.requestFocus()
+ onValueChange(value.copy(selection = TextRange.Zero))
+ showToolbar(Rect(offset, 0f))
+ }
+ )
+ .widthIn(max = with(density) { intrinsics.width.toDp() })
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ // TextField size is changed with a delay (text jumps). Here we correct it.
+ layout(placeable.width, placeable.height) {
+ placeable.place(
+ x = (intrinsics.width - intrinsics.maxIntrinsicWidth)
+ .coerceAtLeast(0f)
+ .roundToInt(),
+ y = (placeable.height - intrinsics.height).roundToInt()
+ )
+ }
}
- )
- .widthIn(max = with(density) { intrinsics.width.toDp() })
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- // TextField size is changed with a delay (text jumps). Here we correct it.
- layout(placeable.width, placeable.height) {
- placeable.place(
- x = (intrinsics.width - intrinsics.maxIntrinsicWidth)
- .coerceAtLeast(0f)
- .roundToInt(),
- y = (placeable.height - intrinsics.height).roundToInt()
+ .onGloballyPositioned { layoutCoords ->
+ offset = layoutCoords.positionInWindow()
+ },
+ textStyle = nTextStyle,
+ cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
+ singleLine = true,
+ readOnly = readOnly,
+ visualTransformation = visualTransformation,
+ decorationBox = { innerTextField ->
+ if (textValue.text.isEmpty() and !placeholder.isNullOrEmpty()) {
+ Text(
+ text = placeholder!!, // It's not null, i swear
+ style = nTextStyle,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
+ modifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+
+ layout(placeable.width, placeable.height) {
+ placeable.place(x = -placeable.width, y = 0)
+ }
+ }
)
}
+
+ innerTextField()
}
- .onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() },
- textStyle = nTextStyle,
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
- singleLine = true,
- readOnly = readOnly,
- interactionSource = interactionSource
- )
+ )
+ }
}
}
@@ -260,8 +308,22 @@ private fun AutoSizableTextField(
*
* @param value Formatted value that has grouping symbols.
*/
-fun ClipboardManager.copyWithoutGrouping(value: TextFieldValue) = this.setText(
+fun ClipboardManager.copyWithoutGrouping(
+ value: TextFieldValue,
+ formatterSymbols: FormatterSymbols
+) = this.setText(
AnnotatedString(
- Formatter.removeGrouping(value.annotatedString.subSequence(value.selection).text)
+ value.annotatedString
+ .subSequence(value.selection)
+ .text
+ .replace(formatterSymbols.grouping, "")
+ )
+)
+
+fun ClipboardManager.copy(value: TextFieldValue) = this.setText(
+ AnnotatedString(
+ value.annotatedString
+ .subSequence(value.selection)
+ .text
)
)
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt
new file mode 100644
index 00000000..5e48fd28
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt
@@ -0,0 +1,59 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+
+fun TextFieldValue.addTokens(tokens: String): TextFieldValue {
+ return this.copy(
+ text = text.replaceRange(selection.start, selection.end, tokens),
+ selection = TextRange(selection.start + tokens.length)
+ )
+}
+
+fun TextFieldValue.deleteTokens(): TextFieldValue {
+ val distanceFromEnd = text.length - selection.end
+
+ val deleteRangeStart = when (selection.end) {
+ // Don't delete if at the start of the text field
+ 0 -> return this
+ // We don't have anything selected (cursor in one position)
+ // like this 1234|56 => after deleting will be like this 123|56
+ // Cursor moved one symbol left
+ selection.start -> {
+ // We default to 1 here. It means that cursor is not placed after illegal token
+ // Just a number or a binary operator or something else, can delete by one symbol
+ val symbolsToDelete = text.tokenLengthAhead(selection.end)
+
+ selection.start - symbolsToDelete
+ }
+ // We have multiple symbols selected
+ // like this 123[45]6 => after deleting will be like this 123|6
+ // Cursor will be placed where selection start was
+ else -> selection.start
+ }
+
+ val newText = text.removeRange(deleteRangeStart, selection.end)
+
+ return this.copy(
+ text = newText,
+ selection = TextRange((newText.length - distanceFromEnd).coerceAtLeast(0))
+ )
+}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt
index f3005c52..d3169397 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt
@@ -20,9 +20,11 @@ package com.sadellie.unitto.core.ui.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Calculate
+import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.outlined.Calculate
+import androidx.compose.material.icons.outlined.Event
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.ui.graphics.vector.ImageVector
@@ -45,6 +47,12 @@ sealed class DrawerItems(
defaultIcon = Icons.Outlined.SwapHoriz
)
+ object DateDifference : DrawerItems(
+ destination = TopLevelDestinations.DateDifference,
+ selectedIcon = Icons.Filled.Event,
+ defaultIcon = Icons.Outlined.Event
+ )
+
object Settings : DrawerItems(
destination = TopLevelDestinations.Settings,
selectedIcon = Icons.Filled.Settings,
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
index 5b90a70e..5740782f 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt
@@ -25,7 +25,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
private val Montserrat = FontFamily(
Font(R.font.montserrat_light, weight = FontWeight.Light),
diff --git a/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt b/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt
new file mode 100644
index 00000000..2e67a81e
--- /dev/null
+++ b/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui
+
+import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class ExpressionTransformerTest {
+
+ private val expr = ExpressionTransformer(FormatterSymbols.Comma)
+
+ private fun origToTrans(orig: String, trans: String, offset: Int): Int =
+ expr.ExpressionMapping(orig, trans).originalToTransformed(offset)
+
+ private fun transToOrig(trans: String, orig: String, offset: Int): Int =
+ expr.ExpressionMapping(orig, trans).transformedToOriginal(offset)
+
+ @Test
+ fun `test 1234`() {
+ // at the start
+ assertEquals(0, origToTrans("1,234", "1234", 0))
+ assertEquals(0, transToOrig("1,234", "1234", 0))
+
+ // somewhere in inside, no offset needed
+ assertEquals(1, origToTrans("1234", "1,234", 1))
+ assertEquals(1, transToOrig("1,234", "1234", 1))
+
+ // somewhere in inside, offset needed
+ assertEquals(1, transToOrig("1,234", "1234", 2))
+
+ // at the end
+ assertEquals(5, origToTrans("1234", "1,234", 4))
+ assertEquals(4, transToOrig("1,234", "1234", 5))
+ }
+
+ @Test
+ fun `test 123`() {
+ // at the start
+ assertEquals(0, origToTrans("123", "123", 0))
+ assertEquals(0, transToOrig("123", "123", 0))
+
+ // somewhere in inside
+ assertEquals(1, origToTrans("123", "123", 1))
+ assertEquals(1, transToOrig("123", "123", 1))
+
+ // at the end
+ assertEquals(3, origToTrans("123", "123", 3))
+ assertEquals(3, transToOrig("123", "123", 3))
+ }
+}
diff --git a/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt b/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt
index cf8cc458..28c87f7c 100644
--- a/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt
+++ b/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt
@@ -20,7 +20,9 @@ package com.sadellie.unitto.core.ui
import android.content.Context
import androidx.compose.ui.test.junit4.createComposeRule
-import com.sadellie.unitto.core.base.Separator
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.formatExpression
+import com.sadellie.unitto.core.ui.common.textfield.formatTime
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -29,16 +31,14 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.math.BigDecimal
-private val formatter = Formatter
-
private const val ENG_VALUE = "123E+21"
private const val ENG_VALUE_FRACTIONAL = "123.3E+21"
private const val COMPLETE_VALUE = "123456.789"
private const val INCOMPLETE_VALUE = "123456."
private const val NO_FRACTIONAL_VALUE = "123456"
-private const val INCOMPLETE_EXPR = "50+123456Ă·8Ă0.8â12+"
-private const val COMPLETE_EXPR = "50+123456Ă·8Ă0.8â12+0-â9*4^9+2Ă(9+8Ă7)"
-private const val LONG_HALF_COMPLETE_EXPR = "50+123456Ă·89078..9Ă0.8â12+0-â9*4^9+2Ă(9+8Ă7)Ăsin(13sin123cos"
+private const val INCOMPLETE_EXPR = "50+123456Ă·8Ă0.8-12+"
+private const val COMPLETE_EXPR = "50+123456Ă·8Ă0.8-12+0-â9Ă4^9+2Ă(9+8Ă7)"
+private const val LONG_HALF_COMPLETE_EXPR = "50+123456Ă·89078..9Ă0.8-12+0-â9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos"
private const val SOME_BRACKETS = "(((((((("
@RunWith(RobolectricTestRunner::class)
@@ -49,167 +49,143 @@ class FormatterTest {
@Test
fun setSeparatorSpaces() {
- formatter.setSeparator(Separator.SPACES)
- assertEquals(".", formatter.fractional)
- assertEquals("123E+21", formatter.format(ENG_VALUE))
- assertEquals("123.3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
- assertEquals("123Â 456.789", formatter.format(COMPLETE_VALUE))
- assertEquals("123Â 456.", formatter.format(INCOMPLETE_VALUE))
- assertEquals("123Â 456", formatter.format(NO_FRACTIONAL_VALUE))
- assertEquals("50+123Â 456Ă·8Ă0.8â12+", formatter.format(INCOMPLETE_EXPR))
- assertEquals("50+123Â 456Ă·8Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", formatter.format(COMPLETE_EXPR))
- assertEquals("50+123Â 456Ă·89Â 078..9Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
- assertEquals("((((((((", formatter.format(SOME_BRACKETS))
+ fun String.format(): String = formatExpression(FormatterSymbols.Spaces)
+ assertEquals("123E+21", ENG_VALUE.format())
+ assertEquals("123.3E+21", ENG_VALUE_FRACTIONAL.format())
+ assertEquals("123Â 456.789", COMPLETE_VALUE.format())
+ assertEquals("123Â 456.", INCOMPLETE_VALUE.format())
+ assertEquals("123Â 456", NO_FRACTIONAL_VALUE.format())
+ assertEquals("50+123Â 456Ă·8Ă0.8â12+", INCOMPLETE_EXPR.format())
+ assertEquals("50+123Â 456Ă·8Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", COMPLETE_EXPR.format())
+ assertEquals("50+123Â 456Ă·89Â 078..9Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
+ assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun setSeparatorComma() {
- formatter.setSeparator(Separator.COMMA)
- assertEquals(".", formatter.fractional)
- assertEquals("123E+21", formatter.format(ENG_VALUE))
- assertEquals("123.3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
- assertEquals("123,456.789", formatter.format(COMPLETE_VALUE))
- assertEquals("123,456.", formatter.format(INCOMPLETE_VALUE))
- assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
- assertEquals("50+123,456Ă·8Ă0.8â12+", formatter.format(INCOMPLETE_EXPR))
- assertEquals("50+123,456Ă·8Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", formatter.format(COMPLETE_EXPR))
- assertEquals("50+123,456Ă·89,078..9Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
- assertEquals("((((((((", formatter.format(SOME_BRACKETS))
+ fun String.format(): String = formatExpression(FormatterSymbols.Comma)
+ assertEquals("123E+21", ENG_VALUE.format())
+ assertEquals("123.3E+21", ENG_VALUE_FRACTIONAL.format())
+ assertEquals("123,456.789", COMPLETE_VALUE.format())
+ assertEquals("123,456.", INCOMPLETE_VALUE.format())
+ assertEquals("123,456", NO_FRACTIONAL_VALUE.format())
+ assertEquals("50+123,456Ă·8Ă0.8â12+", INCOMPLETE_EXPR.format())
+ assertEquals("50+123,456Ă·8Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", COMPLETE_EXPR.format())
+ assertEquals("50+123,456Ă·89,078..9Ă0.8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
+ assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun setSeparatorPeriod() {
- formatter.setSeparator(Separator.PERIOD)
- assertEquals(",", formatter.fractional)
- assertEquals("123E+21", formatter.format(ENG_VALUE))
- assertEquals("123,3E+21", formatter.format(ENG_VALUE_FRACTIONAL))
- assertEquals("123.456,789", formatter.format(COMPLETE_VALUE))
- assertEquals("123.456,", formatter.format(INCOMPLETE_VALUE))
- assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
- assertEquals("50+123.456Ă·8Ă0,8â12+", formatter.format(INCOMPLETE_EXPR))
- assertEquals("50+123.456Ă·8Ă0,8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", formatter.format(COMPLETE_EXPR))
- assertEquals("50+123.456Ă·89.078,,9Ă0,8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
- assertEquals("((((((((", formatter.format(SOME_BRACKETS))
+ fun String.format(): String = formatExpression(FormatterSymbols.Period)
+ assertEquals("123E+21", ENG_VALUE.format())
+ assertEquals("123,3E+21", ENG_VALUE_FRACTIONAL.format())
+ assertEquals("123.456,789", COMPLETE_VALUE.format())
+ assertEquals("123.456,", INCOMPLETE_VALUE.format())
+ assertEquals("123.456", NO_FRACTIONAL_VALUE.format())
+ assertEquals("50+123.456Ă·8Ă0,8â12+", INCOMPLETE_EXPR.format())
+ assertEquals("50+123.456Ă·8Ă0,8â12+0ââ9Ă4^9+2Ă(9+8Ă7)", COMPLETE_EXPR.format())
+ assertEquals("50+123.456Ă·89.078,,9Ă0,8â12+0ââ9Ă4^9+2Ă(9+8Ă7)Ăsin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
+ assertEquals("((((((((", SOME_BRACKETS.format())
}
@Test
fun formatTimeTest() {
- formatter.setSeparator(Separator.SPACES)
+ val formatterSymbols = FormatterSymbols.Spaces
var basicValue = BigDecimal.valueOf(1)
val mContext: Context = RuntimeEnvironment.getApplication().applicationContext
- assertEquals("-28", formatter.formatTime(mContext, "-28", basicValue))
- assertEquals("-0.05", formatter.formatTime(mContext, "-0.05", basicValue))
- assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
- assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
+
+ fun String.formatTime() = this.formatTime(mContext, basicValue, formatterSymbols)
+
+ assertEquals("â28", "-28".formatTime())
+ assertEquals("â0.05", "-0.05".formatTime())
+ assertEquals("0", "0".formatTime())
+ assertEquals("0", "â0".formatTime())
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
- assertEquals("-28d", formatter.formatTime(mContext, "-28", basicValue))
- assertEquals("-1h 12m", formatter.formatTime(mContext, "-0.05", basicValue))
- assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
- assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
+ assertEquals("â28d", "-28".formatTime())
+ assertEquals("â1h 12m", "-0.05".formatTime())
+ assertEquals("0", "0".formatTime())
+ assertEquals("0", "-0".formatTime())
// DAYS
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
- assertEquals("12h", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("1h 12m", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("7m 12s", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28d", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("90d", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("90d 12h", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("90d 7m 12s", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("12h","0.5".formatTime())
+ assertEquals("1h 12m","0.05".formatTime())
+ assertEquals("7m 12s","0.005".formatTime())
+ assertEquals("28d","28".formatTime())
+ assertEquals("90d","90".formatTime())
+ assertEquals("90d 12h","90.5".formatTime())
+ assertEquals("90d 7m 12s","90.005".formatTime())
// HOURS
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
- assertEquals("30m", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("3m", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("18s", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("1d 4h", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("3d 18h", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("3d 18h 30m", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("3d 18h 18s", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("30m", "0.5".formatTime())
+ assertEquals("3m", "0.05".formatTime())
+ assertEquals("18s", "0.005".formatTime())
+ assertEquals("1d 4h", "28".formatTime())
+ assertEquals("3d 18h", "90".formatTime())
+ assertEquals("3d 18h 30m", "90.5".formatTime())
+ assertEquals("3d 18h 18s", "90.005".formatTime())
// MINUTES
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
- assertEquals("30s", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("3s", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("300ms", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28m", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("1h 30m", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("1h 30m 30s", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("1h 30m 300ms", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("30s", "0.5".formatTime())
+ assertEquals("3s", "0.05".formatTime())
+ assertEquals("300ms", "0.005".formatTime())
+ assertEquals("28m", "28".formatTime())
+ assertEquals("1h 30m", "90".formatTime())
+ assertEquals("1h 30m 30s", "90.5".formatTime())
+ assertEquals("1h 30m 300ms", "90.005".formatTime())
// SECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
- assertEquals("500ms", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("50ms", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("5ms", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28s", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("1m 30s", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("1m 30s 500ms", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("1m 30s 5ms", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("500ms", "0.5".formatTime())
+ assertEquals("50ms", "0.05".formatTime())
+ assertEquals("5ms", "0.005".formatTime())
+ assertEquals("28s", "28".formatTime())
+ assertEquals("1m 30s", "90".formatTime())
+ assertEquals("1m 30s 500ms", "90.5".formatTime())
+ assertEquals("1m 30s 5ms", "90.005".formatTime())
// MILLISECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
- assertEquals("500”s", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("50”s", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("5”s", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28ms", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("90ms", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("90ms 500”s", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("90ms 5”s", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("500”s", "0.5".formatTime())
+ assertEquals("50”s", "0.05".formatTime())
+ assertEquals("5”s", "0.005".formatTime())
+ assertEquals("28ms", "28".formatTime())
+ assertEquals("90ms", "90".formatTime())
+ assertEquals("90ms 500”s", "90.5".formatTime())
+ assertEquals("90ms 5”s", "90.005".formatTime())
// MICROSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000)
- assertEquals("500ns", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("50ns", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("5ns", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28”s", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("90”s", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("90”s 500ns", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("90”s 5ns", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("500ns", "0.5".formatTime())
+ assertEquals("50ns", "0.05".formatTime())
+ assertEquals("5ns", "0.005".formatTime())
+ assertEquals("28”s", "28".formatTime())
+ assertEquals("90”s", "90".formatTime())
+ assertEquals("90”s 500ns", "90.5".formatTime())
+ assertEquals("90”s 5ns", "90.005".formatTime())
// NANOSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000)
- assertEquals("500Â 000Â 000as", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("50Â 000Â 000as", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("5Â 000Â 000as", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28ns", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("90ns", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("90ns 500Â 000Â 000as", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("90ns 5Â 000Â 000as", formatter.formatTime(mContext, "90.005", basicValue))
+ assertEquals("500Â 000Â 000as", "0.5".formatTime())
+ assertEquals("50Â 000Â 000as", "0.05".formatTime())
+ assertEquals("5Â 000Â 000as", "0.005".formatTime())
+ assertEquals("28ns", "28".formatTime())
+ assertEquals("90ns", "90".formatTime())
+ assertEquals("90ns 500Â 000Â 000as", "90.5".formatTime())
+ assertEquals("90ns 5Â 000Â 000as", "90.005".formatTime())
// ATTOSECONDS
basicValue = BigDecimal.valueOf(1)
- assertEquals("0.5", formatter.formatTime(mContext, "0.5", basicValue))
- assertEquals("0.05", formatter.formatTime(mContext, "0.05", basicValue))
- assertEquals("0.005", formatter.formatTime(mContext, "0.005", basicValue))
- assertEquals("28", formatter.formatTime(mContext, "28", basicValue))
- assertEquals("90", formatter.formatTime(mContext, "90", basicValue))
- assertEquals("90.5", formatter.formatTime(mContext, "90.5", basicValue))
- assertEquals("90.005", formatter.formatTime(mContext, "90.005", basicValue))
- }
-
- @Test
- fun fromSeparatorToSpacesTest() {
- formatter.setSeparator(Separator.SPACES)
- assertEquals("123Â 456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
- assertEquals("123Â 456.789", formatter.fromSeparator("123Â 456.789", Separator.SPACES))
- assertEquals("123Â 456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
- }
-
- @Test
- fun fromSeparatorToPeriodTest() {
- formatter.setSeparator(Separator.PERIOD)
- assertEquals("123.456,789", formatter.fromSeparator("123,456.789", Separator.COMMA))
- assertEquals("123.456,789", formatter.fromSeparator("123Â 456.789", Separator.SPACES))
- assertEquals("123.456,789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
- }
-
- @Test
- fun fromSeparatorToCommaTest() {
- formatter.setSeparator(Separator.COMMA)
- assertEquals("123,456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
- assertEquals("123,456.789", formatter.fromSeparator("123Â 456.789", Separator.SPACES))
- assertEquals("123,456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
+ assertEquals("0.5", "0.5".formatTime())
+ assertEquals("0.05", "0.05".formatTime())
+ assertEquals("0.005", "0.005".formatTime())
+ assertEquals("28", "28".formatTime())
+ assertEquals("90", "90".formatTime())
+ assertEquals("90.5", "90.5".formatTime())
+ assertEquals("90.005", "90.005".formatTime())
}
}
\ No newline at end of file
diff --git a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorRepository.kt b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorRepository.kt
index 54576a03..743d4576 100644
--- a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorRepository.kt
+++ b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorRepository.kt
@@ -59,6 +59,7 @@ class CalculatorHistoryRepository @Inject constructor(
private fun List.toHistoryItemList(): List {
return this.map {
HistoryItem(
+ id = it.entityId,
date = Date(it.timestamp),
expression = it.expression,
result = it.result
diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts
index ae906d33..6c1f9cf4 100644
--- a/data/common/build.gradle.kts
+++ b/data/common/build.gradle.kts
@@ -26,4 +26,5 @@ android {
dependencies {
implementation(project(mapOf("path" to ":core:base")))
+ testImplementation(libs.junit)
}
\ No newline at end of file
diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
index 16843cf9..0400b469 100644
--- a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
+++ b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
@@ -64,11 +64,7 @@ fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
/**
* 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/data/common/src/main/java/com/sadellie/unitto/data/common/StringUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/StringUtils.kt
index 7e52fd33..dea0e339 100644
--- a/data/common/src/main/java/com/sadellie/unitto/data/common/StringUtils.kt
+++ b/data/common/src/main/java/com/sadellie/unitto/data/common/StringUtils.kt
@@ -18,6 +18,8 @@
package com.sadellie.unitto.data.common
+import com.sadellie.unitto.core.base.Token
+
/**
* Compute Levenshtein Distance between this string and [secondString]. Doesn't matter which string is
* first.
@@ -58,3 +60,18 @@ fun String.lev(secondString: String): Int {
return cost[this.length]
}
+
+fun String.isExpression(): Boolean {
+
+ if (isEmpty()) return false
+
+ // Positive numbers and zero
+ if (all { it.toString() in Token.Digit.allWithDot }) return false
+
+ // Negative numbers
+ // Needs to start with an negative
+ if (this.first().toString() != Token.Operator.minus) return true
+
+ // Rest of the string must be just like positive
+ return this.drop(1).isExpression()
+}
diff --git a/data/common/src/test/java/com/sadellie/unitto/data/common/IsExpressionText.kt b/data/common/src/test/java/com/sadellie/unitto/data/common/IsExpressionText.kt
new file mode 100644
index 00000000..28a5ec48
--- /dev/null
+++ b/data/common/src/test/java/com/sadellie/unitto/data/common/IsExpressionText.kt
@@ -0,0 +1,49 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.data.common
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+
+class IsExpressionText {
+ @Test
+ fun `empty string`() = assertEquals(false, "".isExpression())
+
+ @Test
+ fun `positive real number`() = assertEquals(false, "123".isExpression())
+
+ @Test
+ fun `positive float`() = assertEquals(false, "123.123".isExpression())
+
+ @Test
+ fun `negative real`() = assertEquals(false, "â123".isExpression())
+
+ @Test
+ fun `negative float`() = assertEquals(false, "â123.123".isExpression())
+
+ @Test
+ fun `super negative float`() = assertEquals(false, "ââ123.123".isExpression())
+
+ @Test
+ fun expression1() = assertEquals(true, "123.123+456".isExpression())
+
+ @Test
+ fun expression2() = assertEquals(true, "â123.123+456".isExpression())
+}
diff --git a/data/epoch/src/test/java/com/sadellie/unitto/data/epoch/DateToEpochTest.kt b/data/epoch/src/test/java/com/sadellie/unitto/data/epoch/DateToEpochTest.kt
index 5a4ee4f7..30b963a2 100644
--- a/data/epoch/src/test/java/com/sadellie/unitto/data/epoch/DateToEpochTest.kt
+++ b/data/epoch/src/test/java/com/sadellie/unitto/data/epoch/DateToEpochTest.kt
@@ -18,8 +18,8 @@
package com.sadellie.unitto.data.epoch
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
class DateToEpochTest {
diff --git a/data/unitgroups/.gitignore b/data/evaluatto/.gitignore
similarity index 100%
rename from data/unitgroups/.gitignore
rename to data/evaluatto/.gitignore
diff --git a/data/unitgroups/build.gradle.kts b/data/evaluatto/build.gradle.kts
similarity index 81%
rename from data/unitgroups/build.gradle.kts
rename to data/evaluatto/build.gradle.kts
index 0f477e00..30a2557b 100644
--- a/data/unitgroups/build.gradle.kts
+++ b/data/evaluatto/build.gradle.kts
@@ -18,17 +18,14 @@
plugins {
id("unitto.library")
- id("unitto.android.hilt")
}
android {
- namespace = "com.sadellie.unitto.data.unitgroups"
+ // Different namespace. Possible promotion to a separate project.
+ namespace = "io.github.sadellie.evaluatto"
}
dependencies {
- testImplementation(libs.junit)
- implementation(libs.org.burnoutcrew.composereorderable)
-
implementation(project(mapOf("path" to ":core:base")))
- implementation(project(mapOf("path" to ":data:model")))
+ testImplementation(libs.junit)
}
diff --git a/data/unitgroups/consumer-rules.pro b/data/evaluatto/consumer-rules.pro
similarity index 100%
rename from data/unitgroups/consumer-rules.pro
rename to data/evaluatto/consumer-rules.pro
diff --git a/data/unitgroups/src/main/AndroidManifest.xml b/data/evaluatto/src/main/AndroidManifest.xml
similarity index 100%
rename from data/unitgroups/src/main/AndroidManifest.xml
rename to data/evaluatto/src/main/AndroidManifest.xml
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt
new file mode 100644
index 00000000..b39452e0
--- /dev/null
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt
@@ -0,0 +1,309 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import com.sadellie.unitto.core.base.MAX_PRECISION
+import com.sadellie.unitto.core.base.Token
+import java.math.BigDecimal
+import java.math.MathContext
+import java.math.RoundingMode
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.tan
+import kotlin.math.acos
+import kotlin.math.asin
+import kotlin.math.atan
+import kotlin.math.ln
+import kotlin.math.log
+import kotlin.math.exp
+import kotlin.math.pow
+
+sealed class ExpressionException(override val message: String): Exception(message) {
+ class DivideByZero : ExpressionException("Can't divide by zero")
+ class FactorialCalculation : ExpressionException("Can calculate factorial of non-negative real numbers only")
+ class BadExpression : ExpressionException("Invalid expression. Probably some operator lacks argument")
+ class TooBig : ExpressionException("Value is too big")
+}
+
+class Expression(input: String, private val radianMode: Boolean = true) {
+ private val tokens = Tokenizer(input).tokenize()
+ private var cursorPosition = 0
+
+ /**
+ * Expression := [ "-" ] Term { ("+" | "-") Term }
+ *
+ * Term := Factor { ( "*" | "/" ) Factor }
+ *
+ * Factor := RealNumber | "(" Expression ")"
+ *
+ * RealNumber := Digit{Digit} | [ Digit ] "." {Digit}
+ *
+ * Digit := "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
+ */
+ fun calculate(): BigDecimal {
+ try {
+ return parseExpression()
+ } catch (e: UninitializedPropertyAccessException) {
+ throw ExpressionException.BadExpression()
+ }
+ }
+
+ // Null when at the end of expression
+ private fun peek() = tokens.getOrNull(cursorPosition) ?: ""
+
+ private fun moveIfMatched(token: String): Boolean {
+ if (peek() == token) {
+ // Move cursor
+ cursorPosition++
+ return true
+ }
+ return false
+ }
+
+ // Expression := [ "-" ] Term { ("+" | "-") Term }
+ private fun parseExpression(): BigDecimal {
+ var expression = parseTerm()
+
+ while (peek() in listOf(Token.Operator.plus, Token.Operator.minus)) {
+ when {
+ moveIfMatched(Token.Operator.plus) -> expression += parseTerm()
+ moveIfMatched(Token.Operator.minus) -> expression -= parseTerm()
+ }
+ }
+ return expression
+ }
+
+ // Term := Factor { ( "*" | "/" ) Factor }
+ private fun parseTerm(): BigDecimal {
+ var expression = parseFactor()
+
+ while (peek() in listOf(Token.Operator.multiply, Token.Operator.divide)) {
+ when {
+ moveIfMatched(Token.Operator.multiply) -> expression =
+ expression.multiply(parseFactor())
+
+ moveIfMatched(Token.Operator.divide) -> {
+ val divisor = parseFactor()
+ if (divisor.compareTo(BigDecimal.ZERO) == 0) throw ExpressionException.DivideByZero()
+
+ expression = expression.divide(divisor, RoundingMode.HALF_EVEN)
+ }
+ }
+ }
+ return expression
+ }
+
+ // Factor := RealNumber | "(" Expression ")"
+ private fun parseFactor(negative: Boolean = false): BigDecimal {
+ // This will throw Exception if some function lacks argument, for example: "cos()" or "600^"
+ lateinit var expr: BigDecimal
+
+ fun parseFuncParentheses(): BigDecimal {
+ return if (moveIfMatched(Token.Operator.leftBracket)) {
+ // Parse in parentheses
+ val res = parseExpression()
+
+ // Check if parentheses is closed
+ if (!moveIfMatched(Token.Operator.rightBracket)) throw Exception("Closing bracket is missing")
+ res
+ } else {
+ parseFactor()
+ }
+ }
+
+ // Unary plus
+ if (moveIfMatched(Token.Operator.plus)) return parseFactor()
+
+ // Unary minus
+ if (moveIfMatched(Token.Operator.minus)) {
+ return -parseFactor(true)
+ }
+
+ // Parentheses
+ if (moveIfMatched(Token.Operator.leftBracket)) {
+ // Parse in parentheses
+ expr = parseExpression()
+
+ // Check if parentheses is closed
+ if (!moveIfMatched(Token.Operator.rightBracket)) throw Exception("Closing bracket is missing")
+ }
+
+ // Numbers
+ val possibleNumber = peek()
+ // We know that if next token starts with a digit or dot, it can be converted into BigDecimal
+ // Ugly
+ if (possibleNumber.isNotEmpty()) {
+ if (Token.Digit.allWithDot.contains(possibleNumber.first().toString())) {
+ expr = BigDecimal(possibleNumber).setScale(MAX_PRECISION)
+ cursorPosition++
+ }
+ }
+
+ // PI
+ if (moveIfMatched(Token.Const.pi)) {
+ expr = BigDecimal.valueOf(Math.PI)
+ }
+
+ // e
+ if (moveIfMatched(Token.Const.e)) {
+ expr = BigDecimal.valueOf(Math.E)
+ }
+
+ // sqrt
+ if (moveIfMatched(Token.Operator.sqrt)) {
+ expr = parseFuncParentheses().pow(BigDecimal(0.5))
+ }
+
+ // sin
+ if (moveIfMatched(Token.Func.sin)) {
+ expr = parseFuncParentheses().sin(radianMode)
+ }
+
+ // cos
+ if (moveIfMatched(Token.Func.cos)) {
+ expr = parseFuncParentheses().cos(radianMode)
+ }
+
+ // tan
+ if (moveIfMatched(Token.Func.tan)) {
+ expr = parseFuncParentheses().tan(radianMode)
+ }
+
+ // arsin
+ if (moveIfMatched(Token.Func.arsin)) {
+ expr = parseFuncParentheses().arsin(radianMode)
+ }
+
+ // arcos
+ if (moveIfMatched(Token.Func.arcos)) {
+ expr = parseFuncParentheses().arcos(radianMode)
+ }
+
+ // actan
+ if (moveIfMatched(Token.Func.actan)) {
+ expr = parseFuncParentheses().artan(radianMode)
+ }
+
+ // ln
+ if (moveIfMatched(Token.Func.ln)) {
+ expr = parseFuncParentheses().ln()
+ }
+
+ // log
+ if (moveIfMatched(Token.Func.log)) {
+ expr = parseFuncParentheses().log()
+ }
+
+ // exp
+ if (moveIfMatched(Token.Func.exp)) {
+ expr = parseFuncParentheses().exp()
+ }
+
+ // Power
+ if (moveIfMatched(Token.Operator.power)) {
+ expr = expr.pow(parseFactor())
+ }
+
+ // Modulo
+ if (moveIfMatched(Token.Operator.modulo)) {
+ expr = expr.remainder(parseFactor())
+ }
+
+ // Factorial
+ if (moveIfMatched(Token.Operator.factorial)) {
+ if (negative) throw ExpressionException.FactorialCalculation()
+ expr = expr.factorial()
+ }
+
+ return expr
+ }
+}
+
+private fun BigDecimal.sin(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return sin(angle).toBigDecimal()
+}
+
+private fun BigDecimal.arsin(radianMode: Boolean): BigDecimal {
+ val angle: Double = asin(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+private fun BigDecimal.cos(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return cos(angle).toBigDecimal()
+}
+
+private fun BigDecimal.arcos(radianMode: Boolean): BigDecimal {
+ val angle: Double = acos(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+private fun BigDecimal.tan(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return tan(angle).toBigDecimal()
+}
+
+private fun BigDecimal.artan(radianMode: Boolean): BigDecimal {
+ val angle: Double = atan(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+private fun BigDecimal.ln(): BigDecimal {
+ return ln(this.toDouble()).toBigDecimal()
+}
+
+private fun BigDecimal.log(): BigDecimal {
+ return log(this.toDouble(), 10.0).toBigDecimal()
+}
+
+private fun BigDecimal.exp(): BigDecimal {
+ return exp(this.toDouble()).toBigDecimal()
+}
+
+private fun BigDecimal.pow(n: BigDecimal): BigDecimal {
+ val mathContext: MathContext = MathContext.DECIMAL64
+
+ var right = n
+ val signOfRight = right.signum()
+ right = right.multiply(signOfRight.toBigDecimal())
+ val remainderOfRight = right.remainder(BigDecimal.ONE)
+ val n2IntPart = right.subtract(remainderOfRight)
+ val intPow = pow(n2IntPart.intValueExact(), mathContext)
+ val doublePow = BigDecimal(
+ toDouble().pow(remainderOfRight.toDouble())
+ )
+
+ var result = intPow.multiply(doublePow, mathContext)
+ if (signOfRight == -1) result =
+ BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP)
+
+ return result
+}
+
+private fun BigDecimal.factorial(): BigDecimal {
+ if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation()
+ if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation()
+
+ var expr = this
+ for (i in 1 until this.toInt()) {
+ expr *= BigDecimal(i)
+ }
+ return expr
+}
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Tokenizer.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Tokenizer.kt
new file mode 100644
index 00000000..03da1154
--- /dev/null
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Tokenizer.kt
@@ -0,0 +1,244 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import com.sadellie.unitto.core.base.Token
+
+sealed class TokenizerException(override val message: String) : Exception(message) {
+ class BadNumber : TokenizerException("Number has multiple commas in it")
+}
+
+class Tokenizer(private val streamOfTokens: String) {
+ // Don't create object at all?
+ fun tokenize(): List {
+ var cursor = 0
+ val tokens: MutableList = mutableListOf()
+
+ while (cursor != streamOfTokens.length) {
+ val nextToken = peekTokenAfter(cursor)
+
+ if (nextToken != null) {
+ tokens.add(nextToken)
+ cursor += nextToken.length
+ } else {
+ // Didn't find any token, move left slowly (by 1 symbol)
+ cursor++
+ }
+ }
+
+ return tokens.repairLexicon()
+ }
+
+ private fun peekTokenAfter(cursor: Int): String? {
+ Token.expressionTokens.forEach { token ->
+ val subs = streamOfTokens
+ .substring(
+ cursor,
+ (cursor + token.length).coerceAtMost(streamOfTokens.length)
+ )
+ if (subs == token) {
+ // Got a digit, see if there are other digits coming after
+ if (token in Token.Digit.allWithDot) {
+ val number = streamOfTokens
+ .substring(cursor)
+ .takeWhile { Token.Digit.allWithDot.contains(it.toString()) }
+
+ if (number.count { it.toString() == Token.Digit.dot } > 1) {
+ throw TokenizerException.BadNumber()
+ }
+
+ return number
+ }
+ return token
+ }
+ }
+ return null
+ }
+
+ private fun List.repairLexicon(): List {
+ return this
+ .missingClosingBrackets()
+ .missingMultiply()
+ .unpackAlPercents()
+ // input like 80%80% should be treated as 80%-80%.
+ // After unpacking we get (80/100)(80/100), the multiply is missing
+ // No, we can't unpack before fixing missing multiply.
+ // Ideally we we need to add missing multiply for 80%80%
+ // In that case unpackAlPercents gets input with all operators 80%*80% in this case
+ // Can't be done right now since missingMultiply checks for tokens in front only
+ .missingMultiply()
+ }
+
+ private fun List.missingClosingBrackets(): List {
+ val leftBracket = this.count { it == Token.Operator.leftBracket }
+ val rightBrackets = this.count { it == Token.Operator.rightBracket }
+ val neededBrackets = leftBracket - rightBrackets
+
+ if (neededBrackets <= 0) return this
+
+ var fixed = this
+ repeat(neededBrackets) {
+ fixed = fixed + Token.Operator.rightBracket
+ }
+ return fixed
+ }
+
+ private fun List.missingMultiply(): List {
+ val results = this.toMutableList()
+ val insertIndexes = mutableListOf()
+
+ // Records the index if it needs a multiply symbol
+ fun needsMultiply(index: Int) {
+ val tokenInFront = results.getOrNull(index - 1) ?: return
+
+ when {
+ tokenInFront.first().toString() in Token.Digit.allWithDot ||
+ tokenInFront == Token.Operator.rightBracket ||
+ tokenInFront in Token.Const.all -> {
+ // Can't add token now, it will modify tokens list (we are looping over it)
+ insertIndexes.add(index + insertIndexes.size)
+ }
+ }
+ }
+
+ results.forEachIndexed { index, s ->
+ when (s) {
+ Token.Operator.leftBracket,
+ Token.Operator.sqrt,
+ in Token.Const.all,
+ in Token.Func.all -> needsMultiply(index)
+ }
+ }
+
+ insertIndexes.forEach {
+ results.add(it, Token.Operator.multiply)
+ }
+
+ return results
+ }
+
+ private fun List.unpackAlPercents(): List {
+ var result = this
+ while (result.contains(Token.Operator.percent)) {
+ val percIndex = result.indexOf(Token.Operator.percent)
+ result = result.unpackPercentAt(percIndex)
+ }
+ return result
+ }
+
+ private fun List.unpackPercentAt(percentIndex: Int): List {
+ var cursor = percentIndex
+
+ // get whatever is the percentage
+ val percentage = this.getNumberOrExpressionBefore(percentIndex)
+ // Move cursor
+ cursor -= percentage.size
+
+ // get the operator in front
+ cursor -= 1
+ val operator = this.getOrNull(cursor)
+
+ // Don't go further
+ if ((operator == null) or (operator !in listOf(Token.Operator.plus, Token.Operator.minus))) {
+ val mutList = this.toMutableList()
+
+ // Remove percentage
+ mutList.removeAt(percentIndex)
+
+ //Add opening bracket before percentage
+ mutList.add(percentIndex - percentage.size, Token.Operator.leftBracket)
+
+ // Add "/ 100" and closing bracket
+ mutList.addAll(percentIndex + 1, listOf(Token.Operator.divide, "100", Token.Operator.rightBracket))
+
+ return mutList
+ }
+ // Get the base
+ val base = this.getBaseBefore(cursor)
+ val mutList = this.toMutableList()
+
+ // Remove percentage
+ mutList.removeAt(percentIndex)
+
+ //Add opening bracket before percentage
+ mutList.add(percentIndex - percentage.size, Token.Operator.leftBracket)
+
+ // Add "/ 100" and other stuff
+ mutList.addAll(
+ percentIndex + 1,
+ listOf(
+ Token.Operator.divide,
+ "100",
+ Token.Operator.multiply,
+ Token.Operator.leftBracket,
+ *base.toTypedArray(),
+ Token.Operator.rightBracket,
+ Token.Operator.rightBracket
+ )
+ )
+
+ return mutList
+ }
+
+ private fun List.getNumberOrExpressionBefore(pos: Int): List {
+ val digits = Token.Digit.allWithDot.map { it[0] }
+
+ val tokenInFront = this[pos - 1]
+
+ // Just number
+ if (tokenInFront.all { it in digits }) return listOf(tokenInFront)
+
+ // Not just a number. Probably expression in brackets.
+ if (tokenInFront != Token.Operator.rightBracket) throw Exception("Unexpected token before percentage")
+
+ // Start walking left until we get balanced brackets
+ var cursor = pos - 1
+ var leftBrackets = 0
+ var rightBrackets = 1 // We set 1 because we start with closing bracket
+
+ while (leftBrackets != rightBrackets) {
+ cursor--
+ val currentToken = this[cursor]
+ if (currentToken == Token.Operator.leftBracket) leftBrackets++
+ if (currentToken == Token.Operator.rightBracket) rightBrackets++
+ }
+
+ return this.subList(cursor, pos)
+ }
+
+ private fun List.getBaseBefore(pos: Int): List {
+ var cursor = pos
+ var leftBrackets = 0
+ var rightBrackets = 0
+
+ while ((--cursor >= 0)) {
+ val currentToken = this[cursor]
+
+ if (currentToken == Token.Operator.leftBracket) leftBrackets++
+ if (currentToken == Token.Operator.rightBracket) rightBrackets++
+
+ if (leftBrackets > rightBrackets) break
+ }
+
+ // Return cursor back to last token
+ cursor += 1
+
+ return this.subList(cursor, pos)
+ }
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionComplexTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionComplexTest.kt
new file mode 100644
index 00000000..f7ab5edb
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionComplexTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Test
+
+class ExpressionComplexTest {
+
+ @Test
+ fun expression1() = assertExpr("94ĂÏĂ89Ăcos(0.5)â3!Ă·9^(2)Ăâ8", "23064.9104578494")
+
+ @Test
+ fun expression2() = assertExpr("â(25)Ă2+10Ă·2", "15")
+
+ @Test
+ fun expression3() = assertExpr("(3+4)Ă(5â2)", "21")
+
+ @Test
+ fun expression4() = assertExpr("8Ă·4+2Ă3", "8")
+
+ @Test
+ fun expression5() = assertExpr("2^3+4^2â5Ă6", "-6")
+
+ @Test
+ fun expression6() = assertExpr("(10â2)^2Ă·8+3Ă2", "14")
+
+ @Test
+ fun expression7() = assertExpr("7!Ă·3!â5!Ă·2!", "780")
+
+ @Test
+ fun expression8() = assertExpr("(2^2+3^3)Ă·5ââ(16)Ă2", "-1.8")
+
+ @Test
+ fun expression9() = assertExpr("10Ălog(100)+2^4â3^2", "27")
+
+ @Test
+ fun expression10() = assertExpr("sin(ÏĂ·3)Ăcos(ÏĂ·6)+tan(ÏĂ·4)ââ3", "0.017949192431123")
+
+ @Test
+ fun expression11() = assertExpr("2^6â2^5+2^4â2^3+2^â2^1+2^0", "41.25")
+
+ @Test
+ fun expression12() = assertExpr("2Ă(3+4)Ă(5â2)Ă·6", "7")
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionExceptionsTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionExceptionsTest.kt
new file mode 100644
index 00000000..78ffeb83
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionExceptionsTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Test
+
+class ExpressionExceptionsTest {
+
+ @Test
+ fun `divide by zero`() = assertExprFail(ExpressionException.DivideByZero::class.java, "2Ă·0")
+
+ @Test
+ fun `factorial of float`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "3.2!")
+
+ @Test
+ fun `factorial of negative`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "â5!")
+
+ @Test
+ fun `factorial of negative2`() = assertExprFail(ExpressionException.FactorialCalculation::class.java, "(â5)!")
+
+ @Test
+ fun `ugly ahh expression`() = assertExprFail(ExpressionException.BadExpression::class.java, "100+cos()")
+
+ @Test
+ fun `ugly ahh expression2`() = assertExprFail(TokenizerException.BadNumber::class.java, "...")
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionSimpleTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionSimpleTest.kt
new file mode 100644
index 00000000..0e1f68f8
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/ExpressionSimpleTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Test
+
+class ExpressionSimpleTest {
+
+ @Test
+ fun expression1() = assertExpr("789", "789")
+
+ @Test
+ fun expression2() = assertExpr("0.1+0.2", "0.3")
+
+ @Test
+ fun expression3() = assertExpr(".1+.2", "0.3")
+
+ @Test
+ fun expression4() = assertExpr("789+200", "989")
+
+ @Test
+ fun expression5() = assertExpr("600Ă7.89", "4734")
+
+ @Test
+ fun expression6() = assertExpr("600Ă·7", "85.7142857143")
+
+ @Test
+ fun expression7() = assertExpr("(200+200)Ă200", "80000")
+
+ @Test
+ fun expression8() = assertExpr("99^5", "9509900499")
+
+ @Test
+ fun expression9() = assertExpr("12!", "479001600")
+
+ @Test
+ fun expression10() = assertExpr("12#5", "2")
+
+ @Test
+ fun `125 plus 9 percent`() = assertExpr("125+9%", "136.25")
+
+ @Test
+ fun expression11() = assertExpr("12Ăâ5", "26.8328157300")
+
+ @Test
+ fun expression12() = assertExpr("sin(42)", "-0.9165215479")
+
+ @Test
+ fun expression13() = assertExpr("sin(42)", "0.6691306064", radianMode = false)
+
+ @Test
+ fun expression14() = assertExpr("cos(42)", "-0.3999853150")
+
+ @Test
+ fun expression15() = assertExpr("cos(42)", "0.7431448255", radianMode = false)
+
+ @Test
+ fun expression16() = assertExpr("tan(42)", "2.2913879924")
+
+ @Test
+ fun expression17() = assertExpr("tan(42)", "0.9004040443", radianMode = false)
+
+ @Test
+ fun expression18() = assertExpr("sinâ»Âč(.69)", "0.7614890527")
+
+ @Test
+ fun expression19() = assertExpr("sinâ»Âč(.69)", "43.6301088679", radianMode = false)
+
+ @Test
+ fun expression20() = assertExpr("cosâ»Âč(.69)", "0.8093072740")
+
+ @Test
+ fun expression21() = assertExpr("cosâ»Âč(.69)", "46.3698911321", radianMode = false)
+
+ @Test
+ fun expression22() = assertExpr("tanâ»Âč(.69)", "0.6039829783")
+
+ @Test
+ fun expression23() = assertExpr("tanâ»Âč(.69)", "34.6056755516", radianMode = false)
+
+ @Test
+ fun expression24() = assertExpr("ln(.69)", "-0.3710636814")
+
+ @Test
+ fun expression25() = assertExpr("log(.69)", "-0.1611509093")
+
+ @Test
+ fun expression26() = assertExpr("exp(3)", "20.0855369232")
+
+ @Test
+ fun expression27() = assertExpr("Ï", "3.1415926536")
+
+ @Test
+ fun expression28() = assertExpr("e", "2.7182818285")
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/FixLexiconTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/FixLexiconTest.kt
new file mode 100644
index 00000000..b6b9d8e6
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/FixLexiconTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Test
+
+class FixLexiconTest {
+
+ @Test
+ fun `missing multiply`() {
+ assertLex(
+ "2Ă(69â420)", "2(69â420)"
+ )
+
+ assertLex(
+ "0.Ă(69â420)", "0.(69â420)"
+ )
+
+ assertLex(
+ ".0Ă(69â420)", ".0(69â420)"
+ )
+
+ assertLex(
+ ".Ă(69â420)", ".(69â420)"
+ )
+
+ assertLex(
+ "2Ă(69â420)Ă(23â4)Ăcos(9)Ătan((sinâ»Âč(.9)))",
+ "2(69â420)(23â4)cos(9)tan((sinâ»Âč(.9)))"
+ )
+
+ assertLex(
+ "eĂe+Ï", "ee+Ï"
+ )
+ }
+
+ @Test
+ fun `balanced brackets`() {
+ assertLex(
+ "123Ă(12+4)", "123(12+4"
+ )
+
+ assertLex(
+ "12312+4", "12312+4"
+ )
+
+ assertLex(
+ "123)))12+4", "123)))12+4"
+ )
+
+ assertLex(
+ "sin(cos(tan(3)))", "sin(cos(tan(3"
+ )
+
+ assertLex(
+ "sin(cos(tan(3)))", "sin(cos(tan(3)"
+ )
+
+ assertLex(
+ "sin(cos(tan(3)))", "sin(cos(tan(3))"
+ )
+ }
+
+ @Test
+ fun `unpack percentage`() {
+ // 132.5+14% â> 132.5+132.5*0.14
+ assertLex(
+ "132.5+(14Ă·100Ă(132.5))", "132.5+14%"
+ )
+
+ // 132.5+(14)% â> 132.5+(14)/100*132.5
+ assertLex(
+ "132.5+((14)Ă·100Ă(132.5))" , "132.5+(14)%"
+ )
+
+ // 132.5+(15+4)% â> 132.5+(15+4)*132.5/100
+ assertLex(
+ "132.5+((15+4)Ă·100Ă(132.5))", "132.5+(15+4)%"
+ )
+
+ // (132.5+12%)+(15+4)% â> (132.5+12/100*132.5)+(15+4)/100*(132.5+12/100*132.5)
+ assertLex(
+ "(132.5+(12Ă·100Ă(132.5)))+((15+4)Ă·100Ă((132.5+(12Ă·100Ă(132.5)))))", "(132.5+12%)+(15+4)%"
+ )
+
+ // 2% â> 2/100
+ assertLex(
+ "(2Ă·100)", "2%"
+ )
+
+ assertLex(
+ "((2)Ă·100)", "(2)%"
+ )
+
+ assertLex(
+ "(132.5+5)+(90Ă·100Ă((132.5+5)))", "(132.5+5)+90%"
+ )
+
+ assertLex(
+ "((90Ă·100)+(90Ă·100Ă((90Ă·100))))", "(90%+90%)"
+ )
+
+ assertLex(
+ "((90Ă·100)Ă·(90Ă·100))+((90Ă·100)â(90Ă·100Ă((90Ă·100))))", "(90%Ă·90%)+(90%â90%)"
+ )
+
+ assertLex("(80Ă·100)Ă(80Ă·100)", "80%80%")
+
+ assertLex("10+(2.0Ă·100Ă(10))", "10+2.0%")
+
+ assertLex("10+(2.Ă·100Ă(10))", "10+2.%")
+ }
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/Helpers.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/Helpers.kt
new file mode 100644
index 00000000..806c42e7
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/Helpers.kt
@@ -0,0 +1,46 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+fun assertExpr(expr: String, result: String, radianMode: Boolean = true) =
+ assertEquals(
+ BigDecimal(result).setScale(10, RoundingMode.HALF_EVEN),
+ Expression(expr, radianMode).calculate().setScale(10, RoundingMode.HALF_EVEN)
+ )
+
+fun assertExprFail(
+ expectedThrowable: Class?,
+ expr: String,
+ radianMode: Boolean = true
+) {
+ assertThrows(expectedThrowable) {
+ Expression(expr, radianMode = radianMode).calculate()
+ }
+}
+
+fun assertLex(expected: List, actual: String) =
+ assertEquals(expected, Tokenizer(actual).tokenize())
+
+fun assertLex(expected: String, actual: String) =
+ assertEquals(expected, Tokenizer(actual).tokenize().joinToString(""))
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/TokenizerTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/TokenizerTest.kt
new file mode 100644
index 00000000..7f35b7ef
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/TokenizerTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import org.junit.jupiter.api.Test
+
+class TokenizerTest {
+
+ @Test
+ fun tokens1() = assertLex(listOf("789"), "789")
+
+ @Test
+ fun tokens2() = assertLex(listOf("789", "+", "200"), "789+200")
+
+ @Test
+ fun tokens3() = assertLex(listOf("0.1", "+", "0.2"), "0.1+0.2")
+
+ @Test
+ fun tokens4() = assertLex(listOf(".1", "+", ".2"), ".1+.2")
+
+ @Test
+ fun tokens5() = assertLex(listOf(".1", "+", ".2"), ".1+.2")
+
+ @Test
+ fun tokens6() = assertLex(listOf("789", "+", "200", "+", "cos", "(", "456", ")"), "789+200+cos(456)")
+
+ @Test
+ fun tokens8() = assertLex(emptyList(), "")
+
+ @Test
+ fun tokens9() = assertLex(listOf("e"), "something") // Tokenizer knows "e"
+
+ @Test
+ fun tokens10() = assertLex(emptyList(), "funnyword")
+}
diff --git a/data/licenses/src/main/java/com/sadellie/unitto/data/licenses/Library.kt b/data/licenses/src/main/java/com/sadellie/unitto/data/licenses/Library.kt
index 149bb58b..4fce3ecb 100644
--- a/data/licenses/src/main/java/com/sadellie/unitto/data/licenses/Library.kt
+++ b/data/licenses/src/main/java/com/sadellie/unitto/data/licenses/Library.kt
@@ -28,23 +28,6 @@ data class AppLibrary(
val ALL_LIBRARIES by lazy {
listOf(
- AppLibrary(
- name = "MathParser.org-mXparser",
- dev = "Mariusz Gromada",
- website = "https://github.com/mariuszgromada/MathParser.org-mXparser/",
- license = "Non-Commercial license",
- description = "Math Parser Java Android C# .NET/MONO (.NET Framework, .NET Core, .NET " +
- "Standard, .NET PCL, Xamarin.Android, Xamarin.iOS) CLS Library - a super easy, rich" +
- " and flexible mathematical expression parser (expression evaluator, expression " +
- "provided as plain text / strings) for JAVA and C#."
- ),
- AppLibrary(
- name = "ExprK",
- dev = "Keelar",
- website = "https://github.com/Keelar/ExprK",
- license = "MIT license",
- description = "A simple mathematical expression evaluator for Kotlin and Java, written in Kotlin."
- ),
AppLibrary(
name = "currency-api",
dev = "Fawaz Ahmed (fawazahmed0)",
@@ -122,20 +105,6 @@ val ALL_LIBRARIES by lazy {
license = "Apache-2.0",
description = "Utilities for Jetpack Compose"
),
- AppLibrary(
- name = "firebase-analytics-ktx",
- dev = "Google",
- website = "https://developer.android.com/studio/terms.html",
- license = "ASDKL",
- description = "Library to collect and send usage statistics"
- ),
- AppLibrary(
- name = "firebase-crashlytics-ktx",
- dev = "Google",
- website = "https://developer.android.com/studio/terms.html",
- license = "Apache-2.0",
- description = "Library to collect and send crash logs"
- ),
AppLibrary(
name = "Compose Tooling API",
dev = "The Android Open Source Project",
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/AbstractUnit.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/AbstractUnit.kt
index 3e663423..8ee9aae4 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/AbstractUnit.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/AbstractUnit.kt
@@ -37,7 +37,6 @@ import java.math.BigDecimal
* @property renderedShortName Used as cache. Stores short name string for this specific device. Need for
* search functionality.
* @property isFavorite Whether this unit is favorite.
- * @property isEnabled Whether we need to show this unit or not
* @property pairedUnit Latest paired unit on the right
* @property counter The amount of time this unit was chosen
*/
@@ -50,7 +49,6 @@ abstract class AbstractUnit(
var renderedName: String = String(),
var renderedShortName: String = String(),
var isFavorite: Boolean = false,
- var isEnabled: Boolean = true,
var pairedUnit: String? = null,
var counter: Int = 0
) {
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/DefaultUnit.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/DefaultUnit.kt
index 789158c6..b37a6e01 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/DefaultUnit.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/DefaultUnit.kt
@@ -50,6 +50,9 @@ class DefaultUnit(
value: BigDecimal,
scale: Int
): BigDecimal {
+ // Avoid division by zero
+ if (unitTo.basicUnit.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO
+
return this
.basicUnit
.setScale(MAX_PRECISION)
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/HistoryItem.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/HistoryItem.kt
index f6ab18a6..dc56992c 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/HistoryItem.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/HistoryItem.kt
@@ -18,9 +18,10 @@
package com.sadellie.unitto.data.model
-import java.util.*
+import java.util.Date
data class HistoryItem(
+ val id: Int,
val date: Date,
val expression: String,
val result: String
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/UnitGroup.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/UnitGroup.kt
index 2bb22212..9d90d44c 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/UnitGroup.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/UnitGroup.kt
@@ -19,6 +19,7 @@
package com.sadellie.unitto.data.model
import androidx.annotation.StringRes
+import com.sadellie.unitto.core.base.R
val ALL_UNIT_GROUPS: List by lazy {
UnitGroup.values().toList()
diff --git a/data/units/consumer-rules.pro b/data/units/consumer-rules.pro
index 2acdb785..d9087e3d 100644
--- a/data/units/consumer-rules.pro
+++ b/data/units/consumer-rules.pro
@@ -1,5 +1,15 @@
-repackageclasses
+# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
+# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
+ -keep,allowobfuscation,allowshrinking interface retrofit2.Call
+ -keep,allowobfuscation,allowshrinking class retrofit2.Response
+
+ # With R8 full mode generic signatures are stripped for classes that are not
+ # kept. Suspend functions are wrapped in continuations where the type argument
+ # is used.
+ -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
+
-keepclassmembers class ** {
@com.squareup.moshi.FromJson *;
@com.squareup.moshi.ToJson *;
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
index 83e22057..4680e432 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/AllUnitsRepository.kt
@@ -48,6 +48,9 @@ import com.sadellie.unitto.data.units.collections.temperatureCollection
import com.sadellie.unitto.data.units.collections.timeCollection
import com.sadellie.unitto.data.units.collections.torqueCollection
import com.sadellie.unitto.data.units.collections.volumeCollection
+import com.sadellie.unitto.data.units.remote.CurrencyApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
@@ -123,8 +126,8 @@ class AllUnitsRepository @Inject constructor() {
/**
* Filter [AllUnitsRepository.allUnits] and group them.
*
- * @param hideBrokenCurrencies When set to True will remove [AbstractUnit]s that have
- * [AbstractUnit.isEnabled] set to False, which means that [AbstractUnit] can not be used.
+ * @param hideBrokenUnits When set to True will remove [AbstractUnit]s that have
+ * [AbstractUnit.basicUnit] set to [BigDecimal.ZERO] (comes from currencies API).
* @param chosenUnitGroup If provided will scope list to a specific [UnitGroup].
* @param favoritesOnly When True will filter only [AbstractUnit]s with [AbstractUnit.isFavorite]
* set to True.
@@ -135,7 +138,7 @@ class AllUnitsRepository @Inject constructor() {
* @return Grouped by [UnitGroup] list of [AbstractUnit]s.
*/
fun filterUnits(
- hideBrokenCurrencies: Boolean,
+ hideBrokenUnits: Boolean,
chosenUnitGroup: UnitGroup?,
favoritesOnly: Boolean,
searchQuery: String,
@@ -153,8 +156,8 @@ class AllUnitsRepository @Inject constructor() {
if (favoritesOnly) {
units = units.filter { it.isFavorite }
}
- if (hideBrokenCurrencies) {
- units = units.filter { it.isEnabled }
+ if (hideBrokenUnits) {
+ units = units.filter { it.basicUnit > BigDecimal.ZERO }
}
units = when (sorting) {
@@ -198,22 +201,20 @@ class AllUnitsRepository @Inject constructor() {
/**
* Update [AbstractUnit.basicUnit] properties for currencies from [currencyCollection].
*
- * @param conversions Map: [AbstractUnit.unitId] and [BigDecimal] that will replace current
- * [AbstractUnit.basicUnit].
+ * @param unitFrom Base unit
*/
- fun updateBasicUnitsForCurrencies(
- conversions: Map
- ) {
+ suspend fun updateBasicUnitsForCurrencies(
+ unitFrom: AbstractUnit
+ ) = withContext(Dispatchers.IO) {
+ val conversions: Map = CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId).currency
getCollectionByGroup(UnitGroup.CURRENCY).forEach {
// Getting rates from map. We set ZERO as default so that it can be skipped
val rate = conversions.getOrElse(it.unitId) { BigDecimal.ZERO }
// We make sure that we don't divide by zero
if (rate > BigDecimal.ZERO) {
- it.isEnabled = true
it.basicUnit = BigDecimal.ONE.setScale(MAX_PRECISION).div(rate)
} else {
- // Hiding broken currencies
- it.isEnabled = false
+ it.basicUnit = BigDecimal.ZERO
}
}
}
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Acceleration.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Acceleration.kt
index 208b265b..eb79cde7 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Acceleration.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Acceleration.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val accelerationCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Angle.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Angle.kt
index 232ae30a..269f60e8 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Angle.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Angle.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val angleCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Area.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Area.kt
index 08576248..f224ca18 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Area.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Area.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val areaCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Capacitance.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Capacitance.kt
index 9a540386..c865a7c8 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Capacitance.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Capacitance.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val electrostaticCapacitance: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Currency.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Currency.kt
index cf755987..9b9cf6a0 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Currency.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Currency.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val currencyCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Data.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Data.kt
index 4182148c..15b01d3a 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Data.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Data.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val dataCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/DataTransfer.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/DataTransfer.kt
index 7dfcc19a..2cbf39c9 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/DataTransfer.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/DataTransfer.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val dataTransferCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Energy.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Energy.kt
index 646aed2b..939db2db 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Energy.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Energy.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val energyCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/FlowRate.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/FlowRate.kt
index e7e3951a..962819b2 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/FlowRate.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/FlowRate.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.FlowRateUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val flowRateCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Flux.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Flux.kt
index 2c509193..a4a3a758 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Flux.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Flux.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val fluxCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Force.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Force.kt
index ae7f94c9..7f9d41db 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Force.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Force.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val forceCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Length.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Length.kt
index d57b4991..6710164f 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Length.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Length.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val lengthCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Luminance.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Luminance.kt
index d7b88328..38f9aa54 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Luminance.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Luminance.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val luminanceCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Mass.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Mass.kt
index 4f10d33c..731a24bb 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Mass.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Mass.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val massCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/NumberBase.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/NumberBase.kt
index 05050356..da6bcfbf 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/NumberBase.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/NumberBase.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.NumberBaseUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
internal val numberBaseCollection: List by lazy {
listOf(
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Power.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Power.kt
index a6c5ac0c..abc23823 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Power.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Power.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val powerCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Prefix.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Prefix.kt
index 04d4f044..eb7d2854 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Prefix.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Prefix.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val prefixCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Pressure.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Pressure.kt
index 9d1a7da3..9c3c1e89 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Pressure.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Pressure.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val pressureCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Speed.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Speed.kt
index 8cc7a5ab..5bef34cc 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Speed.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Speed.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val speedCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Temperature.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Temperature.kt
index e84d446b..faa8895b 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Temperature.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Temperature.kt
@@ -19,12 +19,12 @@
package com.sadellie.unitto.data.units.collections
import com.sadellie.unitto.core.base.MAX_PRECISION
-import com.sadellie.unitto.data.model.AbstractUnit
-import com.sadellie.unitto.data.model.UnitGroup
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.trimZeros
+import com.sadellie.unitto.data.model.AbstractUnit
+import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
import java.math.RoundingMode
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Time.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Time.kt
index 964a147b..29f8de12 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Time.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Time.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val timeCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Torque.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Torque.kt
index 867391f1..b1752807 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Torque.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Torque.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
val torqueCollection: List by lazy {
diff --git a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Volume.kt b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Volume.kt
index 8e11d58a..51cac9e4 100644
--- a/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Volume.kt
+++ b/data/units/src/main/java/com/sadellie/unitto/data/units/collections/Volume.kt
@@ -18,11 +18,11 @@
package com.sadellie.unitto.data.units.collections
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.MyUnitIDS
-import com.sadellie.unitto.data.units.R
import java.math.BigDecimal
internal val volumeCollection: List by lazy {
diff --git a/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsRepositoryTest.kt b/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsRepositoryTest.kt
index 6973c59b..a1bfc406 100644
--- a/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsRepositoryTest.kt
+++ b/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsRepositoryTest.kt
@@ -21,8 +21,9 @@ package com.sadellie.unitto.data.units
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.UnitGroup
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.math.BigDecimal
class AllUnitsRepositoryTest {
@@ -34,7 +35,7 @@ class AllUnitsRepositoryTest {
fun filterAllUnitsNoFiltersLeft() {
// No filters applied, empty search query, from Left side list
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = false,
+ hideBrokenUnits = false,
chosenUnitGroup = null,
favoritesOnly = false,
searchQuery = "",
@@ -51,7 +52,7 @@ class AllUnitsRepositoryTest {
// All filters applied, from Left side list
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = false,
+ hideBrokenUnits = false,
chosenUnitGroup = UnitGroup.SPEED,
favoritesOnly = true,
searchQuery = "kilometer per hour",
@@ -68,7 +69,7 @@ class AllUnitsRepositoryTest {
fun filterAllUnitsChosenGroupLeft() {
// Only specific group is needed, left side screen
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = false,
+ hideBrokenUnits = false,
chosenUnitGroup = UnitGroup.TIME,
favoritesOnly = false,
searchQuery = "",
@@ -83,7 +84,7 @@ class AllUnitsRepositoryTest {
allUnitsRepository.getById(MyUnitIDS.kilometer).isFavorite = true
// Only favorite units, left side screen
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = false,
+ hideBrokenUnits = false,
chosenUnitGroup = null,
favoritesOnly = true,
searchQuery = "",
@@ -101,7 +102,7 @@ class AllUnitsRepositoryTest {
// Only search query is entered, other filters are not set, left side screen
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = false,
+ hideBrokenUnits = false,
chosenUnitGroup = null,
favoritesOnly = false,
searchQuery = "kilometer per hour",
@@ -118,10 +119,10 @@ class AllUnitsRepositoryTest {
fun filterAllUnitsHideBrokenCurrencies() {
allUnitsRepository
.getById(MyUnitIDS.currency_btc)
- .apply { isEnabled = false }
+ .apply { basicUnit = BigDecimal.ZERO }
// Hide broken currencies (i.e. cannot be used for conversion at the moment)
val result = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = true,
+ hideBrokenUnits = true,
chosenUnitGroup = UnitGroup.CURRENCY,
favoritesOnly = false,
searchQuery = "",
diff --git a/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsTest.kt b/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsTest.kt
index 314db4d2..e5872ddb 100644
--- a/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsTest.kt
+++ b/data/units/src/test/java/com/sadellie/unitto/data/units/AllUnitsTest.kt
@@ -20,14 +20,11 @@ package com.sadellie.unitto.data.units
import com.sadellie.unitto.data.model.NumberBaseUnit
import com.sadellie.unitto.data.model.UnitGroup
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
import java.math.BigDecimal
-@RunWith(JUnit4::class)
class AllUnitsTest {
// Group and it's tested unit ids
@@ -527,7 +524,7 @@ class AllUnitsTest {
history[unitFrom.group] = content.plus(this)
}
- @After
+ @AfterEach
fun after() {
val unitGroup = history.keys.first()
// GROUP : testedCount / totalCount
diff --git a/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinFilterAndSortTest.kt b/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinFilterAndSortTest.kt
index 95d56a27..63cd6de9 100644
--- a/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinFilterAndSortTest.kt
+++ b/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinFilterAndSortTest.kt
@@ -22,8 +22,8 @@ import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.DefaultUnit
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.sortByLev
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
import java.math.BigDecimal
val baseList: List = listOf(
diff --git a/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinTest.kt b/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinTest.kt
index e9d4df7f..1cdb72bf 100644
--- a/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinTest.kt
+++ b/data/units/src/test/java/com/sadellie/unitto/data/units/LevenshteinTest.kt
@@ -19,8 +19,8 @@
package com.sadellie.unitto.data.units
import com.sadellie.unitto.data.common.lev
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
class LevenshteinTest {
diff --git a/data/units/src/test/java/com/sadellie/unitto/data/units/MinimumRequiredScaleTest.kt b/data/units/src/test/java/com/sadellie/unitto/data/units/MinimumRequiredScaleTest.kt
index 1deaf6d3..28657026 100644
--- a/data/units/src/test/java/com/sadellie/unitto/data/units/MinimumRequiredScaleTest.kt
+++ b/data/units/src/test/java/com/sadellie/unitto/data/units/MinimumRequiredScaleTest.kt
@@ -19,8 +19,8 @@
package com.sadellie.unitto.data.units
import com.sadellie.unitto.data.common.setMinimumRequiredScale
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
import java.math.BigDecimal
class MinimumRequiredScaleTest {
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
index a2c46562..cee3d379 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
@@ -39,6 +39,7 @@ import io.github.sadellie.themmo.MonetMode
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject
@@ -65,19 +66,42 @@ import javax.inject.Inject
*/
data class UserPreferences(
val themingMode: ThemingMode = ThemingMode.AUTO,
- val enableDynamicTheme: Boolean = false,
+ val enableDynamicTheme: Boolean = true,
val enableAmoledTheme: Boolean = false,
val customColor: Color = Color.Unspecified,
val monetMode: MonetMode = MonetMode.TONAL_SPOT,
val digitsPrecision: Int = 3,
- val separator: Int = Separator.SPACES,
+ val separator: Int = Separator.SPACE,
val outputFormat: Int = OutputFormat.PLAIN,
val latestLeftSideUnit: String = MyUnitIDS.kilometer,
val latestRightSideUnit: String = MyUnitIDS.mile,
val shownUnitGroups: List = ALL_UNIT_GROUPS,
val enableVibrations: Boolean = true,
val enableToolsExperiment: Boolean = false,
+ val startingScreen: String = TopLevelDestinations.Calculator.route,
+ val radianMode: Boolean = true,
+ val unitConverterFavoritesOnly: Boolean = false,
+ val unitConverterFormatTime: Boolean = false,
+ val unitConverterSorting: UnitsListSorting = UnitsListSorting.USAGE,
+)
+
+data class UIPreferences(
+ val themingMode: ThemingMode = ThemingMode.AUTO,
+ val enableDynamicTheme: Boolean = false,
+ val enableAmoledTheme: Boolean = false,
+ val customColor: Color = Color.Unspecified,
+ val monetMode: MonetMode = MonetMode.TONAL_SPOT,
val startingScreen: String = TopLevelDestinations.Converter.route,
+)
+
+data class MainPreferences(
+ val digitsPrecision: Int = 3,
+ val separator: Int = Separator.SPACE,
+ val outputFormat: Int = OutputFormat.PLAIN,
+ val latestLeftSideUnit: String = MyUnitIDS.kilometer,
+ val latestRightSideUnit: String = MyUnitIDS.mile,
+ val shownUnitGroups: List = ALL_UNIT_GROUPS,
+ val enableVibrations: Boolean = true,
val radianMode: Boolean = true,
val unitConverterFavoritesOnly: Boolean = false,
val unitConverterFormatTime: Boolean = false,
@@ -112,7 +136,7 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY")
}
- val userPreferencesFlow: Flow = dataStore.data
+ val uiPreferencesFlow: Flow = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
@@ -128,8 +152,29 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val customColor: Color = preferences[PrefsKeys.CUSTOM_COLOR]?.let { Color(it.toULong()) } ?: Color.Unspecified
val monetMode: MonetMode = preferences[PrefsKeys.MONET_MODE]?.let { MonetMode.valueOf(it) }
?: MonetMode.TONAL_SPOT
+ val startingScreen: String = preferences[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Converter.route
+
+ UIPreferences(
+ themingMode = themingMode,
+ enableDynamicTheme = enableDynamicTheme,
+ enableAmoledTheme = enableAmoledTheme,
+ customColor = customColor,
+ monetMode = monetMode,
+ startingScreen = startingScreen
+ )
+ }
+
+ val mainPreferencesFlow: Flow = dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ emit(emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { preferences ->
val digitsPrecision: Int = preferences[PrefsKeys.DIGITS_PRECISION] ?: 3
- val separator: Int = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACES
+ val separator: Int = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACE
val outputFormat: Int = preferences[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
val latestLeftSideUnit: String = preferences[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
val latestRightSideUnit: String = preferences[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
@@ -147,19 +192,12 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
} ?: ALL_UNIT_GROUPS
val enableVibrations: Boolean = preferences[PrefsKeys.ENABLE_VIBRATIONS] ?: true
- val enableToolsExperiment: Boolean = preferences[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
- val startingScreen: String = preferences[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Converter.route
val radianMode: Boolean = preferences[PrefsKeys.RADIAN_MODE] ?: true
val unitConverterFavoritesOnly: Boolean = preferences[PrefsKeys.UNIT_CONVERTER_FAVORITES_ONLY] ?: false
val unitConverterFormatTime: Boolean = preferences[PrefsKeys.UNIT_CONVERTER_FORMAT_TIME] ?: false
val unitConverterSorting: UnitsListSorting = preferences[PrefsKeys.UNIT_CONVERTER_SORTING]?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE
- UserPreferences(
- themingMode = themingMode,
- enableDynamicTheme = enableDynamicTheme,
- enableAmoledTheme = enableAmoledTheme,
- customColor = customColor,
- monetMode = monetMode,
+ MainPreferences(
digitsPrecision = digitsPrecision,
separator = separator,
outputFormat = outputFormat,
@@ -167,8 +205,6 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
latestRightSideUnit = latestRightSideUnit,
shownUnitGroups = shownUnitGroups,
enableVibrations = enableVibrations,
- enableToolsExperiment = enableToolsExperiment,
- startingScreen = startingScreen,
radianMode = radianMode,
unitConverterFavoritesOnly = unitConverterFavoritesOnly,
unitConverterFormatTime = unitConverterFormatTime,
@@ -176,6 +212,31 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
)
}
+ val allPreferencesFlow = combine(
+ mainPreferencesFlow, uiPreferencesFlow
+ ) { main, ui ->
+ return@combine UserPreferences(
+ themingMode = ui.themingMode,
+ enableDynamicTheme = ui.enableDynamicTheme,
+ enableAmoledTheme = ui.enableAmoledTheme,
+ customColor = ui.customColor,
+ monetMode = ui.monetMode,
+ digitsPrecision = main.digitsPrecision,
+ separator = main.separator,
+ outputFormat = main.outputFormat,
+ latestLeftSideUnit = main.latestLeftSideUnit,
+ latestRightSideUnit = main.latestRightSideUnit,
+ shownUnitGroups = main.shownUnitGroups,
+ enableVibrations = main.enableVibrations,
+ enableToolsExperiment = false,
+ startingScreen = ui.startingScreen,
+ radianMode = main.radianMode,
+ unitConverterFavoritesOnly = main.unitConverterFavoritesOnly,
+ unitConverterFormatTime = main.unitConverterFormatTime,
+ unitConverterSorting = main.unitConverterSorting,
+ )
+ }
+
/**
* Update precision preference in DataStore
*
diff --git a/fastlane/metadata/android/en-US/changelogs/21.txt b/fastlane/metadata/android/en-US/changelogs/21.txt
new file mode 100644
index 00000000..a1fb0e4e
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/21.txt
@@ -0,0 +1,7 @@
+"Lilac Luster" update:
+
+- Non-mathematical percentage support
+- New interactive Formatting settings
+- Date difference tool
+- Improved performance and UI/UX
+- Added Italian localization
diff --git a/fastlane/metadata/android/en-US/changelogs/22.txt b/fastlane/metadata/android/en-US/changelogs/22.txt
new file mode 100644
index 00000000..d1f86cf0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/22.txt
@@ -0,0 +1,9 @@
+"Lilac Luster" update:
+
+- Non-mathematical percentage support
+- New interactive Formatting settings
+- Date difference tool
+- Improved performance and UI/UX
+- Added Italian localization
+
+Sorry for crashes!
\ No newline at end of file
diff --git a/fastlane/metadata/android/ru/changelogs/21.txt b/fastlane/metadata/android/ru/changelogs/21.txt
new file mode 100644
index 00000000..cf6f896e
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/21.txt
@@ -0,0 +1,6 @@
+ĐĐ±ĐœĐŸĐČĐ»Đ”ĐœĐžĐ” "Lilac Luster":
+
+- ĐĐ·ĐŒĐ”ĐœĐ”ĐœĐ° ŃĐ°Đ±ĐŸŃа ĐżŃĐŸŃĐ”ĐœŃĐŸĐČ
+- ĐĐœŃĐ”ŃаĐșŃĐžĐČĐœŃĐ” ĐœĐ°ŃŃŃĐŸĐčĐșĐž ŃĐŸŃĐŒĐ°ŃĐžŃĐŸĐČĐ°ĐœĐžŃ
+- ĐĐœŃŃŃŃĐŒĐ”ĐœŃ "Đ Đ°Đ·ĐœĐžŃа ĐŒĐ”Đ¶ĐŽŃ ĐŽĐ°ŃĐ°ĐŒĐž"
+- ĐŁĐ»ŃŃŃĐ”ĐœĐ° ĐżŃĐŸĐžĐ·ĐČĐŸĐŽĐžŃДлŃĐœĐŸŃŃŃ Đž ĐČĐœĐ”ŃĐœĐžĐč ĐČОЎ
diff --git a/fastlane/metadata/android/ru/changelogs/22.txt b/fastlane/metadata/android/ru/changelogs/22.txt
new file mode 100644
index 00000000..ef5f499f
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/22.txt
@@ -0,0 +1,8 @@
+ĐĐ±ĐœĐŸĐČĐ»Đ”ĐœĐžĐ” "Lilac Luster":
+
+- ĐĐ·ĐŒĐ”ĐœĐ”ĐœĐ° ŃĐ°Đ±ĐŸŃа ĐżŃĐŸŃĐ”ĐœŃĐŸĐČ
+- ĐĐœŃĐ”ŃаĐșŃĐžĐČĐœŃĐ” ĐœĐ°ŃŃŃĐŸĐčĐșĐž ŃĐŸŃĐŒĐ°ŃĐžŃĐŸĐČĐ°ĐœĐžŃ
+- ĐĐœŃŃŃŃĐŒĐ”ĐœŃ "Đ Đ°Đ·ĐœĐžŃа ĐŒĐ”Đ¶ĐŽŃ ĐŽĐ°ŃĐ°ĐŒĐž"
+- ĐŁĐ»ŃŃŃĐ”ĐœĐ° ĐżŃĐŸĐžĐ·ĐČĐŸĐŽĐžŃДлŃĐœĐŸŃŃŃ Đž ĐČĐœĐ”ŃĐœĐžĐč ĐČОЎ
+
+ĐĐ·ĐČĐžĐœĐžŃĐ” за ĐșŃаŃĐž!
diff --git a/feature/calculator/build.gradle.kts b/feature/calculator/build.gradle.kts
index 1c7915d0..d0055baf 100644
--- a/feature/calculator/build.gradle.kts
+++ b/feature/calculator/build.gradle.kts
@@ -29,12 +29,11 @@ android {
dependencies {
testImplementation(libs.junit)
- implementation(libs.org.mariuszgromada.math.mxparser)
- implementation(libs.com.github.sadellie.themmo)
implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":data:database")))
implementation(project(mapOf("path" to ":data:calculator")))
implementation(project(mapOf("path" to ":data:model")))
+ implementation(project(mapOf("path" to ":data:evaluatto")))
}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt
index 7edc7036..f99fbd4b 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt
@@ -37,7 +37,6 @@ import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -46,7 +45,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -56,24 +57,27 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.sadellie.unitto.core.base.Separator
-import com.sadellie.unitto.core.ui.Formatter
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.MenuButton
+import com.sadellie.unitto.core.ui.common.SettingsButton
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
-import com.sadellie.unitto.core.ui.common.textfield.InputTextField
+import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
+import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField
import com.sadellie.unitto.data.model.HistoryItem
import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
import com.sadellie.unitto.feature.calculator.components.DragDownView
import com.sadellie.unitto.feature.calculator.components.HistoryList
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Locale
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -89,9 +93,9 @@ internal fun CalculatorRoute(
uiState = uiState.value,
navigateToMenu = navigateToMenu,
navigateToSettings = navigateToSettings,
- addSymbol = viewModel::addSymbol,
- clearSymbols = viewModel::clearSymbols,
- deleteSymbol = viewModel::deleteSymbol,
+ addSymbol = viewModel::addTokens,
+ clearSymbols = viewModel::clearInput,
+ deleteSymbol = viewModel::deleteTokens,
onCursorChange = viewModel::onCursorChange,
toggleAngleMode = viewModel::toggleCalculatorMode,
evaluate = viewModel::evaluate,
@@ -107,20 +111,23 @@ private fun CalculatorScreen(
addSymbol: (String) -> Unit,
clearSymbols: () -> Unit,
deleteSymbol: () -> Unit,
- onCursorChange: (IntRange) -> Unit,
+ onCursorChange: (TextRange) -> Unit,
toggleAngleMode: () -> Unit,
evaluate: () -> Unit,
clearHistory: () -> Unit
) {
- var showClearHistoryButton by rememberSaveable { mutableStateOf(false) }
- var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) }
-
+ val focusManager = LocalFocusManager.current
val dragAmount = remember { Animatable(0f) }
val dragCoroutineScope = rememberCoroutineScope()
- val dragAnimDecay = rememberSplineBasedDecay()
+ val dragAnimSpec = rememberSplineBasedDecay()
- var textThingyHeight by remember { mutableStateOf(0) }
- var historyItemHeight by remember { mutableStateOf(0) }
+ var textThingyHeight by remember { mutableIntStateOf(0) }
+ var historyItemHeight by remember { mutableIntStateOf(0) }
+
+ var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) }
+ val showClearHistoryButton by remember(dragAmount.value, historyItemHeight) {
+ derivedStateOf { dragAmount.value > historyItemHeight }
+ }
UnittoScreenWithTopBar(
title = { Text(stringResource(R.string.calculator)) },
@@ -139,12 +146,7 @@ private fun CalculatorScreen(
}
)
} else {
- IconButton(onClick = navigateToSettings) {
- Icon(
- Icons.Outlined.Settings,
- contentDescription = stringResource(R.string.open_settings_description)
- )
- }
+ SettingsButton(navigateToSettings)
}
}
}
@@ -160,6 +162,8 @@ private fun CalculatorScreen(
.fillMaxSize(),
historyItems = uiState.history,
historyItemHeightCallback = { historyItemHeight = it },
+ formatterSymbols = uiState.formatterSymbols,
+ addTokens = addSymbol,
)
},
textFields = { maxDragAmount ->
@@ -182,17 +186,17 @@ private fun CalculatorScreen(
dragAmount.snapTo(draggedAmount)
}
},
+ onDragStarted = {
+ // Moving composables with focus causes performance drop
+ focusManager.clearFocus(true)
+ },
onDragStopped = { velocity ->
dragCoroutineScope.launch {
- dragAmount.animateDecay(velocity, dragAnimDecay)
+ dragAmount.animateDecay(velocity, dragAnimSpec)
// Snap to closest anchor (0, one history item, all history items)
val draggedAmount = listOf(0, historyItemHeight, maxDragAmount)
.minBy { abs(dragAmount.value.roundToInt() - it) }
- .also {
- // Show button only when fully history view is fully expanded
- showClearHistoryButton = it == maxDragAmount
- }
.toFloat()
dragAmount.animateTo(draggedAmount)
}
@@ -202,28 +206,56 @@ private fun CalculatorScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- InputTextField(
+ ExpressionTextField(
modifier = Modifier
.weight(2f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
- value = uiState.input.copy(
- Formatter.fromSeparator(uiState.input.text, Separator.COMMA)
- ),
+ value = uiState.input,
minRatio = 0.5f,
cutCallback = deleteSymbol,
pasteCallback = addSymbol,
- onCursorChange = onCursorChange
+ onCursorChange = onCursorChange,
+ formatterSymbols = uiState.formatterSymbols
)
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
- InputTextField(
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- value = Formatter.format(uiState.output),
- textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f)
- )
+ when (uiState.output) {
+ is CalculationResult.Default -> {
+ var output by remember(uiState.output) {
+ mutableStateOf(TextFieldValue(uiState.output.text))
+ }
+
+ ExpressionTextField(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ value = output,
+ minRatio = 1f,
+ onCursorChange = { output = output.copy(selection = it) },
+ formatterSymbols = uiState.formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
+ readOnly = true,
+ )
+ }
+
+ else -> {
+ val label = uiState.output.label?.let { stringResource(it) } ?: ""
+
+ UnformattedTextField(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ value = TextFieldValue(label),
+ minRatio = 1f,
+ onCursorChange = {},
+ textColor = MaterialTheme.colorScheme.error,
+ readOnly = true,
+ )
+ }
+ }
+
}
// Handle
Box(
@@ -239,8 +271,11 @@ private fun CalculatorScreen(
},
numPad = {
CalculatorKeyboard(
- modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 4.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
radianMode = uiState.radianMode,
+ fractional = uiState.formatterSymbols.fractional,
allowVibration = uiState.allowVibration,
addSymbol = addSymbol,
clearSymbols = clearSymbols,
@@ -306,6 +341,7 @@ private fun PreviewCalculatorScreen() {
"14.07.2005 23:59:19",
).map {
HistoryItem(
+ id = it.hashCode(),
date = dtf.parse(it)!!,
expression = "12345".repeat(10),
result = "1234"
@@ -315,7 +351,7 @@ private fun PreviewCalculatorScreen() {
CalculatorScreen(
uiState = CalculatorUIState(
input = TextFieldValue("1.2345"),
- output = "1234",
+ output = CalculationResult.Default("1234"),
history = historyItems
),
navigateToMenu = {},
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt
index 36f46269..dfb500ca 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt
@@ -18,13 +18,23 @@
package com.sadellie.unitto.feature.calculator
+import androidx.annotation.StringRes
import androidx.compose.ui.text.input.TextFieldValue
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.data.model.HistoryItem
-internal data class CalculatorUIState(
+data class CalculatorUIState(
val input: TextFieldValue = TextFieldValue(),
- val output: String = "",
+ val output: CalculationResult = CalculationResult.Default(),
val radianMode: Boolean = true,
val history: List = emptyList(),
- val allowVibration: Boolean = false
+ val allowVibration: Boolean = false,
+ val formatterSymbols: FormatterSymbols = FormatterSymbols.Spaces
)
+
+sealed class CalculationResult(@StringRes val label: Int? = null) {
+ data class Default(val text: String = "") : CalculationResult()
+ object DivideByZeroError : CalculationResult(R.string.divide_by_zero_error)
+ object Error : CalculationResult(R.string.error_label)
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt
index 9c1b6081..80bc810f 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt
@@ -18,169 +18,137 @@
package com.sadellie.unitto.feature.calculator
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.addTokens
+import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
import com.sadellie.unitto.data.calculator.CalculatorHistoryRepository
+import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.toStringWith
import com.sadellie.unitto.data.common.trimZeros
-import com.sadellie.unitto.data.userprefs.UserPreferences
+import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
+import io.github.sadellie.evaluatto.Expression
+import io.github.sadellie.evaluatto.ExpressionException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import org.mariuszgromada.math.mxparser.Expression
import java.math.BigDecimal
import javax.inject.Inject
-import org.mariuszgromada.math.mxparser.License as MathParserLicense
-import org.mariuszgromada.math.mxparser.mXparser as MathParser
@HiltViewModel
internal class CalculatorViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val calculatorHistoryRepository: CalculatorHistoryRepository,
- private val textFieldController: TextFieldController
) : ViewModel() {
- private val _userPrefs: StateFlow =
- userPrefsRepository.userPreferencesFlow.stateIn(
+ private val _userPrefs: StateFlow =
+ userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
- UserPreferences()
+ MainPreferences()
)
- private val _output: MutableStateFlow = MutableStateFlow("")
+ private val _input: MutableStateFlow = MutableStateFlow(TextFieldValue())
+ private val _output: MutableStateFlow =
+ MutableStateFlow(CalculationResult.Default())
private val _history = calculatorHistoryRepository.historyFlow
val uiState = combine(
- textFieldController.input, _output, _history, _userPrefs
+ _input, _output, _history, _userPrefs
) { input, output, history, userPrefs ->
return@combine CalculatorUIState(
input = input,
output = output,
radianMode = userPrefs.radianMode,
history = history,
- allowVibration = userPrefs.enableVibrations
+ allowVibration = userPrefs.enableVibrations,
+ formatterSymbols = AllFormatterSymbols.getById(userPrefs.separator)
)
}.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000L), CalculatorUIState()
)
- fun addSymbol(symbol: String) = textFieldController.addToInput(symbol)
+ fun addTokens(tokens: String) = _input.update { it.addTokens(tokens) }
+ fun deleteTokens() = _input.update { it.deleteTokens() }
+ fun clearInput() = _input.update { TextFieldValue() }
+ fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) }
- fun deleteSymbol() = textFieldController.delete()
+ // Called when user clicks "=" on a keyboard
+ fun evaluate() = viewModelScope.launch(Dispatchers.IO) {
+ when (val calculationResult = calculateInput()) {
+ is CalculationResult.Default -> {
+ if (calculationResult.text.isEmpty()) return@launch
- fun clearSymbols() = textFieldController.clearInput()
+ // We can get negative number and they use ugly minus symbol
+ val calculationText = calculationResult.text.replace("-", Token.Operator.minus)
- fun toggleCalculatorMode() {
- viewModelScope.launch {
- userPrefsRepository.updateRadianMode(!_userPrefs.value.radianMode)
+ calculatorHistoryRepository.add(
+ expression = _input.value.text,
+ result = calculationText
+ )
+ _input.update {
+ TextFieldValue(calculationText, TextRange(calculationText.length))
+ }
+ _output.update { CalculationResult.Default() }
+ }
+ // Show the error
+ else -> _output.update { calculationResult }
}
}
- // Called when user clicks "=" on a keyboard
- fun evaluate() {
- // Input and output can change while saving in history. This way we cache it here (i think)
- val output = _output.value
-
- // Output can be empty when input and output are identical (for example when user entered
- // just a number, not expression
- if (output.isEmpty()) return
- if (!Expression(textFieldController.inputTextWithoutFormatting().clean).checkSyntax()) return
-
- // Save to history
- viewModelScope.launch(Dispatchers.IO) {
- calculatorHistoryRepository.add(
- expression = textFieldController.inputTextWithoutFormatting(),
- result = output
- )
- textFieldController.clearInput()
- textFieldController.addToInput(output)
- }
-
- _output.update { "" }
+ fun toggleCalculatorMode() = viewModelScope.launch {
+ userPrefsRepository.updateRadianMode(!_userPrefs.value.radianMode)
}
fun clearHistory() = viewModelScope.launch(Dispatchers.IO) {
calculatorHistoryRepository.clear()
}
- fun onCursorChange(selection: IntRange) = textFieldController.moveCursor(selection)
+ private fun calculateInput(): CalculationResult {
+ val currentInput = _input.value.text
+ // Input is empty or not an expression, don't calculate
+ if (!currentInput.isExpression()) return CalculationResult.Default()
- private fun calculateInput() {
- val currentInput = textFieldController.inputTextWithoutFormatting()
- // Input is empty, don't calculate
- if (currentInput.isEmpty()) {
- _output.update { "" }
- return
+ return try {
+ CalculationResult.Default(
+ Expression(currentInput, radianMode = _userPrefs.value.radianMode)
+ .calculate()
+ .also {
+ if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()
+ }
+ .setMinimumRequiredScale(_userPrefs.value.digitsPrecision)
+ .trimZeros()
+ .toStringWith(_userPrefs.value.outputFormat)
+ )
+ } catch (e: ExpressionException.DivideByZero) {
+ CalculationResult.DivideByZeroError
+ } catch (e: Exception) {
+ CalculationResult.Error
}
-
- val calculated = Expression(currentInput.clean).calculate()
-
- // Calculation error, return empty string
- if (calculated.isNaN() or calculated.isInfinite()) {
- _output.update { "" }
- return
- }
-
- val calculatedBigDecimal = calculated
- .toBigDecimal()
- .setMinimumRequiredScale(_userPrefs.value.digitsPrecision)
- .trimZeros()
-
- // Output will be empty if it's same as input
- try {
- val inputBigDecimal = BigDecimal(currentInput)
-
- // Input and output are identical values
- if (inputBigDecimal.compareTo(calculatedBigDecimal) == 0) {
- _output.update { "" }
- return
- }
- } catch (e: NumberFormatException) {
- // Cannot compare input and output
- }
- _output.update {
- calculatedBigDecimal.toStringWith(_userPrefs.value.outputFormat)
- }
- return
}
- /**
- * Clean input so that there are no syntax errors
- */
- private val String.clean: String
- get() {
- val leftBrackets = count { it.toString() == Token.leftBracket }
- val rightBrackets = count { it.toString() == Token.rightBracket }
- val neededBrackets = leftBrackets - rightBrackets
- return replace(Token.minusDisplay, Token.minus)
- .plus(Token.rightBracket.repeat(neededBrackets.coerceAtLeast(0)))
- }
-
init {
- /**
- * mxParser uses some unnecessary rounding for doubles. It causes expressions like 9999^9999
- * to load CPU very much. We use BigDecimal to achieve same result without CPU overload.
- */
- MathParserLicense.iConfirmNonCommercialUse("Sad Ellie")
- MathParser.setCanonicalRounding(false)
- MathParser.removeBuiltinTokens("log")
- MathParser.modifyBuiltinToken("lg", Token.log.dropLast(1))
-
// Observe and invoke calculation without UI lag.
viewModelScope.launch(Dispatchers.Default) {
- combine(_userPrefs, textFieldController.input) { userPrefs, _ ->
- if (userPrefs.radianMode) MathParser.setRadiansMode() else MathParser.setDegreesMode()
- }.collectLatest {
- calculateInput()
+ merge(_userPrefs, _input).collectLatest {
+ val calculated = calculateInput()
+ _output.update {
+ // Don't show error when simply entering stuff
+ if (calculated !is CalculationResult.Default) CalculationResult.Default() else calculated
+ }
}
}
}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/TextFieldController.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/TextFieldController.kt
deleted file mode 100644
index cca32725..00000000
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/TextFieldController.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2023 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.feature.calculator
-
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
-import com.sadellie.unitto.core.base.Token
-import com.sadellie.unitto.core.base.Separator
-import com.sadellie.unitto.core.ui.UnittoFormatter
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
-import javax.inject.Inject
-import kotlin.math.abs
-
-class TextFieldController @Inject constructor() {
- var input: MutableStateFlow = MutableStateFlow(TextFieldValue())
-
- // Internally we don't care about user preference here, because during composition this
- // symbols will be replaced to those that user wanted.
- // We do this because it adds unnecessary logic: it requires additional logic to observe and
- // react to formatting preferences at this level.
- private val localFormatter: UnittoFormatter by lazy {
- UnittoFormatter().also {
- it.setSeparator(Separator.COMMA)
- }
- }
-
- private val cursorFixer by lazy { CursorFixer() }
-
- fun addToInput(symbols: String) {
-
- val text = input.value.text
- val selection = input.value.selection
- val lastToEndDistance = text.length - selection.end
-
- val newInput = if (text.isEmpty()) {
- symbols
- } else {
- text.replaceRange(selection.start, selection.end, symbols)
- }
-
- val inputFormatted = newInput.fixFormat()
- val newSelectionStartEnd = inputFormatted.length - lastToEndDistance
- val fixedCursor = fixCursor(
- newPosition = newSelectionStartEnd..newSelectionStartEnd,
- currentInput = inputFormatted
- ) ?: newSelectionStartEnd..newSelectionStartEnd
-
- input.update {
- it.copy(
- text = inputFormatted,
- selection = TextRange(fixedCursor)
- )
- }
- }
-
- /**
- * Method to call when pasting from clipbaord. It filters input before calling [addToInput].
- */
- fun pasteSymbols(symbols: String) = addToInput(symbols.filterUnknownSymbols())
-
- fun moveCursor(newPosition: IntRange) {
- input.update {
- it.copy(
- selection = TextRange(fixCursor(newPosition) ?: return)
- )
- }
- }
-
- fun delete() {
- val selection = input.value.selection
- val distanceFromEnd = input.value.text.length - selection.end
-
- val deleteRangeStart = when (selection.end) {
- // Don't delete if at the start of the text field
- 0 -> return
- // We don't have anything selected (cursor in one position)
- // like this 1234|56 => after deleting will be like this 123|56
- // Cursor moved one symbol left
- selection.start -> {
- // We default to 1 here. It means that cursor is not placed after illegal token
- // Just a number or a binary operator or something else, can delete by one symbol
- val amountOfSymbolsToDelete: Int =
- cursorFixer.tokenLengthInFront(input.value.text, selection.end) ?: 1
- selection.start - amountOfSymbolsToDelete
- }
- // We have multiple symbols selected
- // like this 123[45]6 => after deleting will be like this 123|6
- // Cursor will be placed where selection start was
- else -> selection.start
- }
-
- input.update {
- val newText = it.text
- .removeRange(deleteRangeStart, it.selection.end)
- .fixFormat()
- it.copy(
- text = newText,
- selection = TextRange((newText.length - distanceFromEnd).coerceAtLeast(0))
- )
- }
- }
-
- fun clearInput() = input.update { TextFieldValue() }
-
- fun inputTextWithoutFormatting() = input.value.text
- .replace(localFormatter.grouping, "")
- .replace(localFormatter.fractional, Token.dot)
-
- private fun fixCursor(
- newPosition: IntRange,
- currentInput: String = input.value.text
- ): IntRange? {
- if (newPosition.last > currentInput.length) return null
-
- val fixedLeftCursor = cursorFixer.fixCursorIfNeeded(currentInput, newPosition.first)
- val fixedRightCursor = cursorFixer.fixCursorIfNeeded(currentInput, newPosition.last)
-
- return fixedLeftCursor..fixedRightCursor
- }
-
- private fun String.fixFormat(): String = localFormatter.reFormat(this)
-
- private fun String.filterUnknownSymbols() = localFormatter.filterUnknownSymbols(this)
-
- private fun TextRange(range: IntRange): TextRange = TextRange(range.first, range.last)
-
- inner class CursorFixer {
- private val illegalTokens by lazy {
- listOf(
- Token.arSin,
- Token.arCos,
- Token.acTan,
- Token.cos,
- Token.sin,
- Token.exp,
- Token.ln,
- Token.log,
- Token.tan
- )
- }
-
- fun fixCursorIfNeeded(str: String, pos: Int): Int {
- // Best position if we move cursor left
- val bestLeft = bestPositionLeft(str, pos)
- // Best position if we move cursor right
- val bestRight = bestPositionRight(str, pos)
-
- return listOf(bestLeft, bestRight)
- .minBy { abs(it - pos) }
- }
-
- fun tokenLengthInFront(str: String, pos: Int): Int? {
- illegalTokens.forEach {
- if (pos.afterToken(str, it)) return it.length
- }
-
- return null
- }
-
- private fun bestPositionLeft(str: String, pos: Int): Int {
- var cursorPosition = pos
- while (placedIllegally(str, cursorPosition)) cursorPosition--
- return cursorPosition
- }
-
- private fun bestPositionRight(str: String, pos: Int): Int {
- var cursorPosition = pos
- while (placedIllegally(str, cursorPosition)) cursorPosition++
- return cursorPosition
- }
-
- private fun placedIllegally(str: String, pos: Int): Boolean {
- // For things like "123,|456" - this is illegal
- if (pos.afterToken(str, localFormatter.grouping)) return true
-
- // For things like "123,456+c|os(8)" - this is illegal
- illegalTokens.forEach {
- if (pos.atToken(str, it)) return true
- }
-
- return false
- }
-
- /**
- * Don't use if token is 1 symbol long, it wouldn't make sense! Use [afterToken] instead.
- * @see [afterToken]
- */
- private fun Int.atToken(str: String, token: String): Boolean {
- val checkBound = (token.length - 1).coerceAtLeast(1)
-
- val stringToScan = str.substring(
- startIndex = (this - checkBound).coerceAtLeast(0),
- endIndex = (this + checkBound).coerceAtMost(str.length)
- )
-
- return stringToScan.contains(token)
- }
-
- private fun Int.afterToken(str: String, token: String): Boolean {
- val stringToScan = str.substring(
- startIndex = (this - token.length).coerceAtLeast(0),
- endIndex = this
- )
-
- return stringToScan.contains(token)
- }
- }
-}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt
index 16d1f1a6..36068f37 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt
@@ -52,7 +52,6 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.sadellie.unitto.core.base.Token
-import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
@@ -103,6 +102,7 @@ import com.sadellie.unitto.core.ui.common.key.unittoicons.Tan
internal fun CalculatorKeyboard(
modifier: Modifier,
radianMode: Boolean,
+ fractional: String,
allowVibration: Boolean,
addSymbol: (String) -> Unit,
clearSymbols: () -> Unit,
@@ -114,6 +114,7 @@ internal fun CalculatorKeyboard(
PortraitKeyboard(
modifier = modifier,
radianMode = radianMode,
+ fractional = fractional,
allowVibration = allowVibration,
addSymbol = addSymbol,
toggleAngleMode = toggleAngleMode,
@@ -125,6 +126,7 @@ internal fun CalculatorKeyboard(
LandscapeKeyboard(
modifier = modifier,
radianMode = radianMode,
+ fractional = fractional,
allowVibration = allowVibration,
addSymbol = addSymbol,
toggleAngleMode = toggleAngleMode,
@@ -139,6 +141,7 @@ internal fun CalculatorKeyboard(
private fun PortraitKeyboard(
modifier: Modifier,
radianMode: Boolean,
+ fractional: String,
allowVibration: Boolean,
addSymbol: (String) -> Unit,
toggleAngleMode: () -> Unit,
@@ -146,7 +149,7 @@ private fun PortraitKeyboard(
clearSymbols: () -> Unit,
evaluate: () -> Unit
) {
- val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma }
+ val fractionalIcon = remember { if (fractional == Token.Digit.dot) UnittoIcons.Dot else UnittoIcons.Comma }
var showAdditional: Boolean by remember { mutableStateOf(false) }
var invMode: Boolean by remember { mutableStateOf(false) }
val expandRotation: Float by animateFloatAsState(
@@ -217,32 +220,32 @@ private fun PortraitKeyboard(
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
Row(weightModifier) {
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.rightBracket) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.Operator.leftBracket) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.Operator.rightBracket) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.Operator.percent) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.Operator.divide) }
}
Row(weightModifier) {
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key9, allowVibration) { addSymbol(Token._9) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Multiply, allowVibration) { addSymbol(Token.multiplyDisplay) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token.Digit._7) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token.Digit._8) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key9, allowVibration) { addSymbol(Token.Digit._9) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Multiply, allowVibration) { addSymbol(Token.Operator.multiply) }
}
Row(weightModifier) {
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key4, allowVibration) { addSymbol(Token._4) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key5, allowVibration) { addSymbol(Token._5) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key6, allowVibration) { addSymbol(Token._6) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Minus, allowVibration) { addSymbol(Token.minusDisplay) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key4, allowVibration) { addSymbol(Token.Digit._4) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key5, allowVibration) { addSymbol(Token.Digit._5) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key6, allowVibration) { addSymbol(Token.Digit._6) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Minus, allowVibration) { addSymbol(Token.Operator.minus) }
}
Row(weightModifier) {
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key1, allowVibration) { addSymbol(Token._1) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key2, allowVibration) { addSymbol(Token._2) }
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key3, allowVibration) { addSymbol(Token._3) }
- KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Plus, allowVibration) { addSymbol(Token.plus) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key1, allowVibration) { addSymbol(Token.Digit._1) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key2, allowVibration) { addSymbol(Token.Digit._2) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key3, allowVibration) { addSymbol(Token.Digit._3) }
+ KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Plus, allowVibration) { addSymbol(Token.Operator.plus) }
}
Row(weightModifier) {
- KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key0, allowVibration) { addSymbol(Token._0) }
- KeyboardButtonLight(mainButtonModifier, fractionalIcon, allowVibration) { addSymbol(Token.dot) }
+ KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key0, allowVibration) { addSymbol(Token.Digit._0) }
+ KeyboardButtonLight(mainButtonModifier, fractionalIcon, allowVibration) { addSymbol(Token.Digit.dot) }
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
}
@@ -263,24 +266,24 @@ private fun AdditionalButtonsPortrait(
) {
Column {
Row {
- KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
- KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.factorial) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.Operator.sqrt) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.Const.pi) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.Operator.power) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.Operator.factorial) }
}
AnimatedVisibility(showAdditional) {
Column {
Row {
KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() }
- KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.Func.sinBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.Func.cosBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.Func.tanBracket) }
}
Row {
KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
- KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.log) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.Const.e) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.Func.lnBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.Func.logBracket) }
}
}
}
@@ -299,24 +302,24 @@ private fun AdditionalButtonsPortraitInverse(
) {
Column {
Row {
- KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
- KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.factorial) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.Operator.modulo) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.Const.pi) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.Operator.power) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.Operator.factorial) }
}
AnimatedVisibility(showAdditional) {
Column {
Row {
KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() }
- KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) }
- KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) }
- KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.Func.arsinBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.Func.arcosBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.Func.actanBracket) }
}
Row {
KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
- KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.exp) }
- KeyboardButtonAdditional(modifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.log) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.Const.e) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.Func.expBracket) }
+ KeyboardButtonAdditional(modifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.Func.logBracket) }
}
}
}
@@ -327,6 +330,7 @@ private fun AdditionalButtonsPortraitInverse(
private fun LandscapeKeyboard(
modifier: Modifier,
radianMode: Boolean,
+ fractional: String,
allowVibration: Boolean,
addSymbol: (String) -> Unit,
toggleAngleMode: () -> Unit,
@@ -334,7 +338,7 @@ private fun LandscapeKeyboard(
clearSymbols: () -> Unit,
evaluate: () -> Unit
) {
- val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma }
+ val fractionalIcon = remember { if (fractional == Token.Digit.dot) UnittoIcons.Dot else UnittoIcons.Comma }
var invMode: Boolean by remember { mutableStateOf(false) }
RowWithConstraints(modifier) { constraints ->
@@ -370,34 +374,34 @@ private fun LandscapeKeyboard(
}
Column(Modifier.weight(1f)) {
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key4, allowVibration) { addSymbol(Token._4) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key1, allowVibration) { addSymbol(Token._1) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key0, allowVibration) { addSymbol(Token._0) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token.Digit._7) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key4, allowVibration) { addSymbol(Token.Digit._4) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key1, allowVibration) { addSymbol(Token.Digit._1) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key0, allowVibration) { addSymbol(Token.Digit._0) }
}
Column(Modifier.weight(1f)) {
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key5, allowVibration) { addSymbol(Token._5) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key2, allowVibration) { addSymbol(Token._2) }
- KeyboardButtonLight(buttonModifier, fractionalIcon, allowVibration) { addSymbol(Token.dot) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token.Digit._8) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key5, allowVibration) { addSymbol(Token.Digit._5) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key2, allowVibration) { addSymbol(Token.Digit._2) }
+ KeyboardButtonLight(buttonModifier, fractionalIcon, allowVibration) { addSymbol(Token.Digit.dot) }
}
Column(Modifier.weight(1f)) {
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key9, allowVibration) { addSymbol(Token._9) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key6, allowVibration) { addSymbol(Token._6) }
- KeyboardButtonLight(buttonModifier, UnittoIcons.Key3, allowVibration) { addSymbol(Token._3) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key9, allowVibration) { addSymbol(Token.Digit._9) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key6, allowVibration) { addSymbol(Token.Digit._6) }
+ KeyboardButtonLight(buttonModifier, UnittoIcons.Key3, allowVibration) { addSymbol(Token.Digit._3) }
KeyboardButtonLight(buttonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() }
}
Column(Modifier.weight(1f)) {
- KeyboardButtonFilled(buttonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) }
- KeyboardButtonFilled(buttonModifier, UnittoIcons.Multiply, allowVibration) { addSymbol(Token.multiplyDisplay) }
- KeyboardButtonFilled(buttonModifier, UnittoIcons.Minus, allowVibration) { addSymbol(Token.minusDisplay) }
- KeyboardButtonFilled(buttonModifier, UnittoIcons.Plus, allowVibration) { addSymbol(Token.plus) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.Operator.leftBracket) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.Multiply, allowVibration) { addSymbol(Token.Operator.multiply) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.Minus, allowVibration) { addSymbol(Token.Operator.minus) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.Plus, allowVibration) { addSymbol(Token.Operator.plus) }
}
Column(Modifier.weight(1f)) {
- KeyboardButtonFilled(buttonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.rightBracket) }
- KeyboardButtonFilled(buttonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) }
- KeyboardButtonFilled(buttonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.Operator.rightBracket) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.Operator.divide) }
+ KeyboardButtonFilled(buttonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.Operator.percent) }
KeyboardButtonFilled(buttonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
}
}
@@ -416,22 +420,22 @@ private fun AdditionalButtonsLandscape(
Column(modifier) {
KeyboardButtonAdditional(buttonModifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() }
KeyboardButtonAdditional(buttonModifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.Func.sinBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.E, allowVibration) { addSymbol(Token.Const.e) }
}
Column(modifier) {
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.Operator.sqrt) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.Operator.power) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.Func.cosBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.Func.lnBracket) }
}
Column(modifier) {
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.factorial) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.log) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.Const.pi) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.Operator.factorial) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.Func.tanBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.Func.logBracket) }
}
}
@@ -448,22 +452,22 @@ private fun AdditionalButtonsLandscapeInverse(
Column(modifier) {
KeyboardButtonAdditional(buttonModifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() }
KeyboardButtonAdditional(buttonModifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.Func.arsinBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.E, allowVibration) { addSymbol(Token.Const.e) }
}
Column(modifier) {
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.exp) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.Operator.modulo) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.Operator.power) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.Func.arcosBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.Func.expBracket) }
}
Column(modifier) {
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.factorial) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) }
- KeyboardButtonAdditional(buttonModifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.log) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.Const.pi) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Factorial, allowVibration) { addSymbol(Token.Operator.factorial) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.Func.actanBracket) }
+ KeyboardButtonAdditional(buttonModifier, UnittoIcons.Log, allowVibration) { addSymbol(Token.Func.logBracket) }
}
}
@@ -473,6 +477,7 @@ private fun PreviewCalculatorKeyboard() {
CalculatorKeyboard(
modifier = Modifier,
radianMode = true,
+ fractional = ".",
addSymbol = {},
clearSymbols = {},
deleteSymbol = {},
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
index 071cf31e..2c7112cd 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
@@ -20,13 +20,17 @@ package com.sadellie.unitto.feature.calculator.components
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
@@ -36,7 +40,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -54,64 +58,101 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.Formatter
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar
+import com.sadellie.unitto.core.ui.common.textfield.clearAndFilterExpression
import com.sadellie.unitto.core.ui.common.textfield.copyWithoutGrouping
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
import com.sadellie.unitto.data.model.HistoryItem
-import com.sadellie.unitto.feature.calculator.R
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Locale
@Composable
internal fun HistoryList(
modifier: Modifier,
historyItems: List,
historyItemHeightCallback: (Int) -> Unit,
+ formatterSymbols: FormatterSymbols,
+ addTokens: (String) -> Unit,
) {
- val verticalArrangement by remember(historyItems) {
- derivedStateOf {
- if (historyItems.isEmpty()) {
- Arrangement.Center
- } else {
- Arrangement.spacedBy(16.dp, Alignment.Bottom)
- }
+ if (historyItems.isEmpty()) {
+ HistoryListPlaceholder(
+ modifier = modifier,
+ historyItemHeightCallback = historyItemHeightCallback
+ )
+ } else {
+ HistoryListContent(
+ modifier = modifier,
+ historyItems = historyItems,
+ addTokens = addTokens,
+ formatterSymbols = formatterSymbols,
+ historyItemHeightCallback = historyItemHeightCallback
+ )
+ }
+}
+
+@Composable
+private fun HistoryListPlaceholder(
+ modifier: Modifier,
+ historyItemHeightCallback: (Int) -> Unit
+) {
+ Column(
+ modifier = modifier.wrapContentHeight(unbounded = true),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Column(
+ modifier = Modifier
+ .onPlaced { historyItemHeightCallback(it.size.height) }
+ .fillMaxWidth()
+ .padding(vertical = 32.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(Icons.Default.History, null)
+ Text(stringResource(R.string.calculator_no_history))
}
}
+}
+
+@Composable
+private fun HistoryListContent(
+ modifier: Modifier,
+ historyItems: List,
+ addTokens: (String) -> Unit,
+ formatterSymbols: FormatterSymbols,
+ historyItemHeightCallback: (Int) -> Unit
+) {
+ val state = rememberLazyListState()
+ val firstItem by remember(historyItems) { mutableStateOf(historyItems.first()) }
+ val restOfTheItems by remember(firstItem) { mutableStateOf(historyItems.drop(1)) }
+
+ LaunchedEffect(historyItems) { state.scrollToItem(0) }
LazyColumn(
modifier = modifier,
+ state = state,
reverseLayout = true,
- verticalArrangement = verticalArrangement
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom)
) {
- if (historyItems.isEmpty()) {
- item {
- Column(
- modifier = Modifier
- .onPlaced { historyItemHeightCallback(it.size.height) }
- .fillParentMaxWidth()
- .padding(vertical = 32.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Icon(Icons.Default.History, null)
- Text(stringResource(R.string.calculator_no_history))
- }
- }
- } else {
- // We do this so that callback for items height is called only once
- item {
- HistoryListItem(
- modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) },
- historyItem = historyItems.first()
- )
- }
- items(historyItems.drop(1)) { historyItem ->
- HistoryListItem(
- modifier = Modifier,
- historyItem = historyItem
- )
- }
+ // We do this so that callback for items height is called only once
+ item(firstItem.id) {
+ HistoryListItem(
+ modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) },
+ historyItem = historyItems.first(),
+ formatterSymbols = formatterSymbols,
+ addTokens = addTokens,
+ )
+ }
+
+ items(restOfTheItems, { it.id }) { historyItem ->
+ HistoryListItem(
+ modifier = Modifier,
+ historyItem = historyItem,
+ formatterSymbols = formatterSymbols,
+ addTokens = addTokens,
+ )
}
}
}
@@ -120,23 +161,42 @@ internal fun HistoryList(
private fun HistoryListItem(
modifier: Modifier = Modifier,
historyItem: HistoryItem,
+ formatterSymbols: FormatterSymbols,
+ addTokens: (String) -> Unit,
) {
val clipboardManager = LocalClipboardManager.current
- val expression = Formatter.format(historyItem.expression)
+ val expression = historyItem.expression.take(1000)
var expressionValue by remember(expression) {
mutableStateOf(TextFieldValue(expression, TextRange(expression.length)))
}
- val result = Formatter.format(historyItem.result)
+ val result = historyItem.result.take(1000)
var resultValue by remember(result) {
mutableStateOf(TextFieldValue(result, TextRange(result.length)))
}
+ val expressionInteractionSource = remember(expression) { MutableInteractionSource() }
+ LaunchedEffect(expressionInteractionSource) {
+ expressionInteractionSource.interactions.collect {
+ if (it is PressInteraction.Release) addTokens(expression.clearAndFilterExpression(formatterSymbols))
+ }
+ }
+
+ val resultInteractionSource = remember(result) { MutableInteractionSource() }
+ LaunchedEffect(resultInteractionSource) {
+ resultInteractionSource.interactions.collect {
+ if (it is PressInteraction.Release) addTokens(result.clearAndFilterExpression(formatterSymbols))
+ }
+ }
+
Column(modifier = modifier) {
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
- copyCallback = { clipboardManager.copyWithoutGrouping(expressionValue) }
+ copyCallback = {
+ clipboardManager.copyWithoutGrouping(expressionValue, formatterSymbols)
+ expressionValue = expressionValue.copy(selection = TextRange(expressionValue.selection.end))
+ }
)
) {
BasicTextField(
@@ -148,7 +208,9 @@ private fun HistoryListItem(
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End),
- readOnly = true
+ readOnly = true,
+ visualTransformation = ExpressionTransformer(formatterSymbols),
+ interactionSource = expressionInteractionSource
)
}
@@ -156,7 +218,10 @@ private fun HistoryListItem(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
- copyCallback = { clipboardManager.copyWithoutGrouping(resultValue) }
+ copyCallback = {
+ clipboardManager.copyWithoutGrouping(resultValue, formatterSymbols)
+ resultValue = resultValue.copy(selection = TextRange(resultValue.selection.end))
+ }
)
) {
BasicTextField(
@@ -168,7 +233,9 @@ private fun HistoryListItem(
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.End),
- readOnly = true
+ readOnly = true,
+ visualTransformation = ExpressionTransformer(formatterSymbols),
+ interactionSource = resultInteractionSource
)
}
}
@@ -190,6 +257,7 @@ private fun PreviewHistoryList() {
"14.07.2005 23:59:19",
).map {
HistoryItem(
+ id = it.hashCode(),
date = dtf.parse(it)!!,
expression = "12345".repeat(10),
result = "67890"
@@ -200,6 +268,9 @@ private fun PreviewHistoryList() {
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.fillMaxSize(),
- historyItems = historyItems
- ) {}
+ historyItems = historyItems,
+ formatterSymbols = FormatterSymbols.Spaces,
+ historyItemHeightCallback = {},
+ addTokens = {}
+ )
}
diff --git a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/TextFieldControllerTest.kt b/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/TextFieldControllerTest.kt
deleted file mode 100644
index ee4d727c..00000000
--- a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/TextFieldControllerTest.kt
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2023 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.feature.calculator
-
-import com.sadellie.unitto.core.base.Separator
-import com.sadellie.unitto.core.ui.Formatter
-import org.junit.Assert.assertEquals
-import org.junit.Before
-import org.junit.Test
-
-internal class TextFieldControllerTest {
- private lateinit var textFieldController: TextFieldController
-
- private val TextFieldController.text: String
- get() = this.input.value.text
-
- private val TextFieldController.selection: IntRange
- get() = this.input.value.selection.start..this.input.value.selection.end
-
- @Before
- fun setUp() {
- textFieldController = TextFieldController()
- Formatter.setSeparator(Separator.COMMA)
- }
-
- @Test
- fun `Add when empty`() {
- // Add one symbol
- textFieldController.addToInput("1")
- assertEquals("1", textFieldController.text)
- assertEquals(1..1, textFieldController.selection)
- textFieldController.clearInput()
-
- // Add multiple
- textFieldController.addToInput("123")
- assertEquals("123", textFieldController.text)
- assertEquals(3..3, textFieldController.selection)
- textFieldController.clearInput()
-
- // Add multiple
- textFieldController.addToInput("1234")
- assertEquals("1,234", textFieldController.text)
- assertEquals(5..5, textFieldController.selection)
- textFieldController.clearInput()
-
- // Add multiple
- textFieldController.addToInput("123456.789")
- assertEquals("123,456.789", textFieldController.text)
- assertEquals(11..11, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Add when not empty one symbol at a time (check formatting)`() {
- // Should be 1|
- textFieldController.addToInput("1")
- assertEquals("1", textFieldController.text)
- assertEquals(1..1, textFieldController.selection)
-
- // Should be 12|
- textFieldController.addToInput("2")
- assertEquals("12", textFieldController.text)
- assertEquals(2..2, textFieldController.selection)
-
- // Should be 123|
- textFieldController.addToInput("3")
- assertEquals("123", textFieldController.text)
- assertEquals(3..3, textFieldController.selection)
-
- // Should be 1,234|
- textFieldController.addToInput("4")
- assertEquals("1,234", textFieldController.text)
- assertEquals(5..5, textFieldController.selection)
-
- // Should be 12,345|
- textFieldController.addToInput("5")
- assertEquals("12,345", textFieldController.text)
- assertEquals(6..6, textFieldController.selection)
- }
-
- @Test
- fun `Delete on empty input`() {
- // Delete on empty input
- textFieldController.delete()
- assertEquals("", textFieldController.text)
- assertEquals(0..0, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete last remaining symbol`() {
- textFieldController.addToInput("1")
- textFieldController.delete()
- assertEquals("", textFieldController.text)
- assertEquals(0..0, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete by one symbol (check formatting)`() {
- textFieldController.addToInput("123456")
- // Input is formatted into 123,456
- textFieldController.delete()
- assertEquals("12,345", textFieldController.text)
- assertEquals(6..6, textFieldController.selection)
- textFieldController.delete()
- assertEquals("1,234", textFieldController.text)
- assertEquals(5..5, textFieldController.selection)
- textFieldController.delete()
- assertEquals("123", textFieldController.text)
- println("in 123: ${textFieldController.selection}")
- assertEquals(3..3, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete multiple symbols, selected before separator`() {
- textFieldController.addToInput("123789456")
- // Input is formatted to 123,789,456
- textFieldController.moveCursor(3..7)
- textFieldController.delete()
- assertEquals("123,456", textFieldController.text)
- assertEquals(3..3, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete multiple symbols, selected not near separator`() {
- textFieldController.addToInput("123789456")
- // Input is formatted to 123,789,456
- textFieldController.moveCursor(3..9)
- textFieldController.delete()
- assertEquals("12,356", textFieldController.text)
- assertEquals(4..4, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete multiple symbols in weird input`() {
- textFieldController.addToInput("123...789456")
- // Input is formatted to 123...789456
- textFieldController.moveCursor(3..9)
- textFieldController.delete()
- assertEquals(4..4, textFieldController.selection)
- assertEquals("123,456", textFieldController.text)
- textFieldController.clearInput()
- }
-
- @Test
- fun `Delete illegal token when cursor is placed after it`() {
- textFieldController.addToInput("cos(sin(ln(log(tan(")
- textFieldController.delete()
- assertEquals("cos(sin(ln(log(", textFieldController.text)
- assertEquals(15..15, textFieldController.selection)
-
- textFieldController.delete()
- assertEquals("cos(sin(ln(", textFieldController.text)
- assertEquals(11..11, textFieldController.selection)
-
- textFieldController.delete()
- assertEquals("cos(sin(", textFieldController.text)
- assertEquals(8..8, textFieldController.selection)
-
- textFieldController.delete()
- assertEquals("cos(", textFieldController.text)
- assertEquals(4..4, textFieldController.selection)
-
- textFieldController.delete()
- assertEquals("", textFieldController.text)
- assertEquals(0..0, textFieldController.selection)
-
- textFieldController.addToInput("1234")
- // Place cursor like 1|,234
- textFieldController.moveCursor(1..1)
- textFieldController.delete()
- assertEquals("234", textFieldController.text)
- assertEquals(0..0, textFieldController.selection)
- }
-
- @Test
- fun `Place cursor illegally`() {
- textFieldController.addToInput("123456.789")
- // Input is 123,456.789
- textFieldController.moveCursor(4..4)
- // Cursor should be placed like this 123|,456.789
- assertEquals(3..3, textFieldController.selection)
- textFieldController.clearInput()
-
- textFieldController.addToInput("123456.789+cos(")
- // Input is 123,456.789+cos(
- textFieldController.moveCursor(13..13)
- // Cursor should be placed like this 123,456.789+c|os(
- assertEquals(12..12, textFieldController.selection)
- textFieldController.clearInput()
- }
-
- @Test
- fun `get clear input text without formatting`() {
- textFieldController.addToInput("123456.789+cos(..)")
- // Input is 123,456.789
- assertEquals("123456.789+cos(..)", textFieldController.inputTextWithoutFormatting())
- }
-
- @Test
- fun `Paste completely weird stuff`() {
- textFieldController.pasteSymbols("crazy stuff from clipboard")
- assertEquals("", textFieldController.text)
- }
-
- @Test
- fun `Paste partially weird stuff`() {
- textFieldController.pasteSymbols("some crazy stuff cos(8+9)*7= that user may have in clipboard")
- assertEquals("ecos(8+9)Ă7ee", textFieldController.text)
- }
-
- @Test
- fun `Paste acceptable stuff that needs symbol replacement`() {
- textFieldController.pasteSymbols("cos(8+9)*7")
- assertEquals("cos(8+9)Ă7", textFieldController.text)
- }
-
- @Test
- fun `Paste acceptable stuff that does not need replacement`() {
- textFieldController.pasteSymbols("cos(8+9)Ă7")
- assertEquals("cos(8+9)Ă7", textFieldController.text)
- }
-
- @Test
- fun `Paste nothing`() {
- textFieldController.pasteSymbols("")
- assertEquals("", textFieldController.text)
- }
-}
diff --git a/feature/converter/build.gradle.kts b/feature/converter/build.gradle.kts
index 3a94684b..296ff6c4 100644
--- a/feature/converter/build.gradle.kts
+++ b/feature/converter/build.gradle.kts
@@ -30,14 +30,11 @@ android {
dependencies {
testImplementation(libs.junit)
testImplementation(libs.org.jetbrains.kotlinx.coroutines.test)
- testImplementation(libs.org.robolectric)
testImplementation(libs.androidx.room.runtime)
testImplementation(libs.androidx.room.ktx)
kapt(libs.androidx.room.compiler)
testImplementation(libs.androidx.datastore)
- implementation(libs.com.github.sadellie.exprk)
- implementation(libs.com.github.sadellie.themmo)
implementation(libs.com.squareup.moshi)
implementation(libs.com.squareup.retrofit2)
@@ -46,4 +43,5 @@ dependencies {
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":data:units")))
+ implementation(project(mapOf("path" to ":data:evaluatto")))
}
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt
index 6ec8c0be..7f27e604 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt
@@ -20,24 +20,21 @@ package com.sadellie.unitto.feature.converter
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Settings
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.MenuButton
import com.sadellie.unitto.core.ui.common.PortraitLandscape
+import com.sadellie.unitto.core.ui.common.SettingsButton
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
import com.sadellie.unitto.feature.converter.components.Keyboard
import com.sadellie.unitto.feature.converter.components.TopScreenPart
@@ -46,11 +43,11 @@ import com.sadellie.unitto.feature.converter.components.TopScreenPart
internal fun ConverterRoute(
viewModel: ConverterViewModel = hiltViewModel(),
navigateToLeftScreen: (String) -> Unit,
- navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit,
+ navigateToRightScreen: (unitFrom: String, unitTo: String, input: String?) -> Unit,
navigateToMenu: () -> Unit,
navigateToSettings: () -> Unit
) {
- val uiState = viewModel.uiStateFlow.collectAsStateWithLifecycle()
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle()
ConverterScreen(
uiState = uiState.value,
@@ -59,9 +56,12 @@ internal fun ConverterRoute(
navigateToSettings = navigateToSettings,
navigateToMenu = navigateToMenu,
swapMeasurements = viewModel::swapUnits,
- processInput = viewModel::processInput,
- deleteDigit = viewModel::deleteDigit,
+ processInput = viewModel::addTokens,
+ deleteDigit = viewModel::deleteTokens,
clearInput = viewModel::clearInput,
+ onCursorChange = viewModel::onCursorChange,
+ cutCallback = viewModel::deleteTokens,
+ onErrorClick = viewModel::updateCurrenciesRatesIfNeeded,
)
}
@@ -69,30 +69,30 @@ internal fun ConverterRoute(
private fun ConverterScreen(
uiState: ConverterUIState,
navigateToLeftScreen: (String) -> Unit,
- navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit,
+ navigateToRightScreen: (unitFrom: String, unitTo: String, input: String?) -> Unit,
navigateToSettings: () -> Unit,
navigateToMenu: () -> Unit,
swapMeasurements: () -> Unit,
processInput: (String) -> Unit,
deleteDigit: () -> Unit,
clearInput: () -> Unit,
+ onCursorChange: (TextRange) -> Unit,
+ cutCallback: () -> Unit,
+ onErrorClick: () -> Unit,
) {
UnittoScreenWithTopBar(
title = { Text(stringResource(R.string.unit_converter)) },
navigationIcon = { MenuButton { navigateToMenu() } },
actions = {
- IconButton(onClick = navigateToSettings) {
- Icon(
- Icons.Outlined.Settings,
- contentDescription = stringResource(R.string.open_settings_description)
- )
- }
+ SettingsButton(navigateToSettings)
},
colors = TopAppBarDefaults
.centerAlignedTopAppBarColors(containerColor = Color.Transparent),
content = { padding ->
PortraitLandscape(
- modifier = Modifier.padding(padding).fillMaxSize(),
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize(),
content1 = {
TopScreenPart(
modifier = it,
@@ -101,13 +101,15 @@ private fun ConverterScreen(
outputValue = uiState.resultValue,
unitFrom = uiState.unitFrom,
unitTo = uiState.unitTo,
- networkLoading = uiState.showLoading,
- networkError = uiState.showError,
navigateToLeftScreen = navigateToLeftScreen,
navigateToRightScreen = navigateToRightScreen,
swapUnits = swapMeasurements,
converterMode = uiState.mode,
- formatTime = uiState.formatTime
+ onCursorChange = onCursorChange,
+ cutCallback = cutCallback,
+ pasteCallback = processInput,
+ formatterSymbols = uiState.formatterSymbols,
+ onErrorClick = onErrorClick
)
},
content2 = {
@@ -117,7 +119,8 @@ private fun ConverterScreen(
deleteDigit = deleteDigit,
clearInput = clearInput,
converterMode = uiState.mode,
- allowVibration = uiState.allowVibration
+ allowVibration = uiState.allowVibration,
+ fractional = uiState.formatterSymbols.fractional,
)
}
)
@@ -125,14 +128,6 @@ private fun ConverterScreen(
)
}
-class PreviewUIState: PreviewParameterProvider {
- override val values: Sequence
- get() = listOf(
- ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false),
- ConverterUIState(inputValue = "1234", calculatedValue = "234", resultValue = "5678", showLoading = false),
- ).asSequence()
-}
-
@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait")
@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait")
@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait")
@@ -140,11 +135,9 @@ class PreviewUIState: PreviewParameterProvider {
@Preview(heightDp = 432, widthDp = 864, device = "spec:parent=pixel_5,orientation=landscape")
@Preview(heightDp = 597, widthDp = 1393, device = "spec:parent=pixel_5,orientation=landscape")
@Composable
-private fun PreviewConverterScreen(
- @PreviewParameter(PreviewUIState::class) uiState: ConverterUIState
-) {
+private fun PreviewConverterScreen() {
ConverterScreen(
- uiState = ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false),
+ uiState = ConverterUIState(inputValue = TextFieldValue("1234"), calculatedValue = null, resultValue = ConversionResult.Default("5678"), showLoading = false),
navigateToLeftScreen = {},
navigateToRightScreen = {_, _, _ -> },
navigateToSettings = {},
@@ -153,5 +146,8 @@ private fun PreviewConverterScreen(
processInput = {},
deleteDigit = {},
clearInput = {},
+ onCursorChange = {},
+ cutCallback = {},
+ onErrorClick = {},
)
-}
\ No newline at end of file
+}
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterUIState.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterUIState.kt
index 6ba34cd6..f225cd85 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterUIState.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterUIState.kt
@@ -18,7 +18,9 @@
package com.sadellie.unitto.feature.converter
+import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.data.model.AbstractUnit
/**
@@ -33,23 +35,30 @@ import com.sadellie.unitto.data.model.AbstractUnit
* @property unitFrom Unit on the left.
* @property unitTo Unit on the right.
* @property mode
- * @property formatTime If true will format output when converting time.
* @property allowVibration When true will vibrate on button clicks.
*/
data class ConverterUIState(
- val inputValue: String = Token._0,
+ val inputValue: TextFieldValue = TextFieldValue(),
val calculatedValue: String? = null,
- val resultValue: String = Token._0,
+ val resultValue: ConversionResult = ConversionResult.Default(Token.Digit._0),
val showLoading: Boolean = true,
val showError: Boolean = false,
val unitFrom: AbstractUnit? = null,
val unitTo: AbstractUnit? = null,
val mode: ConverterMode = ConverterMode.DEFAULT,
- val formatTime: Boolean = true,
- val allowVibration: Boolean = false
+ val allowVibration: Boolean = false,
+ val formatterSymbols: FormatterSymbols = FormatterSymbols.Spaces,
)
enum class ConverterMode {
DEFAULT,
BASE,
}
+
+sealed class ConversionResult {
+ data class Default(val result: String) : ConversionResult()
+ data class Time(val result: String) : ConversionResult()
+ data class NumberBase(val result: String) : ConversionResult()
+ object Loading : ConversionResult()
+ object Error : ConversionResult()
+}
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt
index cd5b64b5..016bfe31 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterViewModel.kt
@@ -18,11 +18,14 @@
package com.sadellie.unitto.feature.converter
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.keelar.exprk.ExpressionException
-import com.github.keelar.exprk.Expressions
-import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.addTokens
+import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
+import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.toStringWith
import com.sadellie.unitto.data.common.trimZeros
@@ -34,13 +37,12 @@ import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.units.AllUnitsRepository
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.combine
-import com.sadellie.unitto.data.units.remote.CurrencyApi
-import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
-import com.sadellie.unitto.data.userprefs.UserPreferences
+import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
+import io.github.sadellie.evaluatto.Expression
+import io.github.sadellie.evaluatto.ExpressionException
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -50,11 +52,8 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import java.math.BigDecimal
-import java.math.RoundingMode
import javax.inject.Inject
@HiltViewModel
@@ -64,10 +63,10 @@ class ConverterViewModel @Inject constructor(
private val allUnitsRepository: AllUnitsRepository
) : ViewModel() {
- private val _userPrefs = userPrefsRepository.userPreferencesFlow.stateIn(
+ private val _userPrefs = userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
- UserPreferences()
+ MainPreferences()
)
/**
@@ -80,25 +79,17 @@ class ConverterViewModel @Inject constructor(
*/
private val _unitTo: MutableStateFlow = MutableStateFlow(null)
- /**
- * Current input. Used when converting units.
- */
- private val _input: MutableStateFlow = MutableStateFlow(Token._0)
+ private val _input: MutableStateFlow = MutableStateFlow(TextFieldValue())
/**
* Calculation result. Null when [_input] is not an expression.
*/
private val _calculated: MutableStateFlow = MutableStateFlow(null)
- /**
- * List of latest symbols that were entered.
- */
- private val _latestInputStack: MutableList = mutableListOf(_input.value)
-
/**
* Conversion result.
*/
- private val _result: MutableStateFlow = MutableStateFlow(Token._0)
+ private val _result: MutableStateFlow = MutableStateFlow(ConversionResult.Loading)
/**
* True when loading something from network.
@@ -113,190 +104,38 @@ class ConverterViewModel @Inject constructor(
/**
* Current state of UI.
*/
- val uiStateFlow: StateFlow = combine(
+ val uiState: StateFlow = combine(
_input,
_unitFrom,
_unitTo,
_calculated,
_result,
- _showLoading,
+ _userPrefs,
_showError,
- _userPrefs
- ) { inputValue, unitFromValue, unitToValue, calculatedValue, resultValue, showLoadingValue, showErrorValue, prefs ->
+ _showLoading
+ ) { inputValue, unitFromValue, unitToValue, calculatedValue, resultValue, prefs, showError, showLoading ->
return@combine ConverterUIState(
inputValue = inputValue,
calculatedValue = calculatedValue,
- resultValue = resultValue,
- showLoading = showLoadingValue,
- showError = showErrorValue,
+ resultValue = when {
+ showError -> ConversionResult.Error
+ showLoading -> ConversionResult.Loading
+ else -> resultValue
+ },
unitFrom = unitFromValue,
unitTo = unitToValue,
- /**
- * If there will be more modes, this should be a separate value which we update when
- * changing units.
- */
mode = if (_unitFrom.value is NumberBaseUnit) ConverterMode.BASE else ConverterMode.DEFAULT,
- formatTime = prefs.unitConverterFormatTime,
- allowVibration = prefs.enableVibrations
- )
- }
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(5000),
- ConverterUIState()
+ allowVibration = prefs.enableVibrations,
+ formatterSymbols = AllFormatterSymbols.getById(prefs.separator)
)
+ }.stateIn(
+ viewModelScope, SharingStarted.WhileSubscribed(5000), ConverterUIState()
+ )
- /**
- * Process input with rules. Makes sure that user input is corrected when needed.
- *
- * @param symbolToAdd Use 'ugly' version of symbols.
- */
- fun processInput(symbolToAdd: String) {
- val lastTwoSymbols = _latestInputStack.takeLast(2)
- val lastSymbol: String = lastTwoSymbols.getOrNull(1) ?: lastTwoSymbols[0]
- val lastSecondSymbol: String? = lastTwoSymbols.getOrNull(0)
-
- when (symbolToAdd) {
- Token.plus, Token.divide, Token.multiply, Token.exponent -> {
- when {
- // Don't need expressions that start with zero
- (_input.value == Token._0) -> {}
- (_input.value == Token.minus) -> {}
- (lastSymbol == Token.leftBracket) -> {}
- (lastSymbol == Token.sqrt) -> {}
- /**
- * For situations like "50+-", when user clicks "/" we delete "-" so it becomes
- * "50+". We don't add "/' here. User will click "/" second time and the input
- * will be "50/".
- */
- (lastSecondSymbol in Token.operators) and (lastSymbol == Token.minus) -> {
- deleteDigit()
- }
- // Don't allow multiple operators near each other
- (lastSymbol in Token.operators) -> {
- deleteDigit()
- setInputSymbols(symbolToAdd)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token._0 -> {
- when {
- // Don't add zero if the input is already a zero
- (_input.value == Token._0) -> {}
- (lastSymbol == Token.rightBracket) -> {
- processInput(Token.multiply)
- setInputSymbols(symbolToAdd)
- }
- // Prevents things like "-00" and "4+000"
- ((lastSecondSymbol in Token.operators + Token.leftBracket) and (lastSymbol == Token._0)) -> {}
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token._1, Token._2, Token._3, Token._4, Token._5,
- Token._6, Token._7, Token._8, Token._9 -> {
- // Replace single zero (default input) if it's here
- when {
- (_input.value == Token._0) -> {
- setInputSymbols(symbolToAdd, false)
- }
- (lastSymbol == Token.rightBracket) -> {
- processInput(Token.multiply)
- setInputSymbols(symbolToAdd)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token.minus -> {
- when {
- // Replace single zero with minus (to support negative numbers)
- (_input.value == Token._0) -> {
- setInputSymbols(symbolToAdd, false)
- }
- // Don't allow multiple minuses near each other
- (lastSymbol.compareTo(Token.minus) == 0) -> {}
- // Don't allow plus and minus be near each other
- (lastSymbol == Token.plus) -> {
- deleteDigit()
- setInputSymbols(symbolToAdd)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token.dot -> {
- if (!_input.value
- .takeLastWhile { it.toString() !in Token.operators.minus(Token.dot) }
- .contains(Token.dot)
- ) {
- setInputSymbols(symbolToAdd)
- }
- }
- Token.leftBracket -> {
- when {
- // Replace single zero with minus (to support negative numbers)
- (_input.value == Token._0) -> {
- setInputSymbols(symbolToAdd, false)
- }
- (lastSymbol == Token.rightBracket) || (lastSymbol in Token.digits) || (lastSymbol == Token.dot) -> {
- processInput(Token.multiply)
- setInputSymbols(symbolToAdd)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token.rightBracket -> {
- when {
- // Replace single zero with minus (to support negative numbers)
- (_input.value == Token._0) -> {}
- (lastSymbol == Token.leftBracket) -> {}
- (
- _latestInputStack.filter { it == Token.leftBracket }.size ==
- _latestInputStack.filter { it == Token.rightBracket }.size
- ) -> {
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- Token.sqrt -> {
- when {
- // Replace single zero with minus (to support negative numbers)
- (_input.value == Token._0) -> {
- setInputSymbols(symbolToAdd, false)
- }
- (lastSymbol == Token.rightBracket) || (lastSymbol in Token.digits) || (lastSymbol == Token.dot) -> {
- processInput(Token.multiply)
- setInputSymbols(symbolToAdd)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- else -> {
- when {
- // Replace single zero with minus (to support negative numbers)
- (_input.value == Token._0) -> {
- setInputSymbols(symbolToAdd, false)
- }
- else -> {
- setInputSymbols(symbolToAdd)
- }
- }
- }
- }
- }
+ fun addTokens(tokens: String) = _input.update { it.addTokens(tokens) }
+ fun deleteTokens() = _input.update { it.deleteTokens() }
+ fun clearInput() = _input.update { TextFieldValue() }
+ fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) }
/**
* Update [_unitFrom] and set [_unitTo] from pair. Also updates stats for this [unit].
@@ -321,8 +160,8 @@ class ConverterViewModel @Inject constructor(
_unitTo.update { allUnitsRepository.getCollectionByGroup(unit.group).first() }
}
incrementCounter(unit)
- updateCurrenciesRatesIfNeeded()
saveLatestPairOfUnits()
+ updateCurrenciesRatesIfNeeded()
}
/**
@@ -343,136 +182,80 @@ class ConverterViewModel @Inject constructor(
_unitFrom
.getAndUpdate { _unitTo.value }
.also { oldUnitFrom -> _unitTo.update { oldUnitFrom } }
+ saveLatestPairOfUnits()
updateCurrenciesRatesIfNeeded()
}
- /**
- * Delete last symbol from [_input].
- */
- fun deleteDigit() {
- // Default input, don't delete
- if (_input.value == Token._0) return
-
- val lastSymbol = _latestInputStack.removeLast()
-
- // If this value are same, it means that after deleting there will be no symbols left, set to default
- if (lastSymbol == _input.value) {
- setInputSymbols(Token._0, false)
- } else {
- _input.update { it.removeSuffix(lastSymbol) }
+ private fun convertInput() {
+ when (_unitFrom.value?.group) {
+ UnitGroup.NUMBER_BASE -> convertAsNumberBase()
+ else -> convertAsExpression()
}
}
- /**
- * Clear [_input].
- */
- fun clearInput() {
- setInputSymbols(Token._0, false)
- }
-
- private suspend fun convertInput() {
- withContext(Dispatchers.Default) {
- while (isActive) {
- when (_unitFrom.value?.group) {
- UnitGroup.NUMBER_BASE -> convertAsNumberBase()
- else -> convertAsExpression()
- }
- cancel()
- }
- }
- }
-
- private fun convertAsNumberBase() {
+ private fun convertAsNumberBase() = viewModelScope.launch(Dispatchers.Default) {
// Units are still loading, don't convert anything yet
- val unitFrom = _unitFrom.value ?: return
- val unitTo = _unitTo.value ?: return
+ val unitFrom = _unitFrom.value ?: return@launch
+ val unitTo = _unitTo.value ?: return@launch
val conversionResult = try {
(unitFrom as NumberBaseUnit).convertToBase(
- input = _input.value,
+ input = _input.value.text.ifEmpty { "0" },
toBase = (unitTo as NumberBaseUnit).base
)
} catch (e: Exception) {
when (e) {
- is ClassCastException -> return
+ is ClassCastException -> return@launch
is NumberFormatException, is IllegalArgumentException -> ""
else -> throw e
}
}
- _result.update { conversionResult }
+ _result.update { ConversionResult.NumberBase(conversionResult) }
}
- private fun convertAsExpression() {
- // Units are still loading, don't convert anything yet
- val unitFrom = _unitFrom.value ?: return
- val unitTo = _unitTo.value ?: return
+ private fun convertAsExpression() = viewModelScope.launch(Dispatchers.Default) {
+ val unitFrom = _unitFrom.value ?: return@launch
+ val unitTo = _unitTo.value ?: return@launch
+ val input = _input.value.text.ifEmpty { "0" }
- // First we clean the input from garbage at the end
- var cleanInput = _input.value.dropLastWhile { !it.isDigit() }
-
- // Now we close open brackets that user didn't close
- // AUTOCLOSE ALL BRACKETS
- val leftBrackets = _input.value.count { it.toString() == Token.leftBracket }
- val rightBrackets = _input.value.count { it.toString() == Token.rightBracket }
- val neededBrackets = leftBrackets - rightBrackets
- if (neededBrackets > 0) cleanInput += Token.rightBracket.repeat(neededBrackets)
-
- // Now we evaluate expression in input
- val evaluationResult: BigDecimal = try {
- Expressions().eval(cleanInput)
- .setScale(_userPrefs.value.digitsPrecision, RoundingMode.HALF_EVEN)
- .trimZeros()
- } catch (e: Exception) {
- when (e) {
- is ExpressionException,
- is ArrayIndexOutOfBoundsException,
- is IndexOutOfBoundsException,
- is NumberFormatException,
- is ArithmeticException -> {
- // Invalid expression, can't do anything further
- return
- }
- else -> throw e
- }
- }
-
- // Evaluated. Hide calculated result if no expression entered.
- // 123.456 will be true
- // -123.456 will be true
- // -123.456-123 will be false (first minus gets removed, ending with 123.456)
- if (_input.value.removePrefix(Token.minus).all { it.toString() !in Token.operators }) {
- // No operators
+ if (input.isEmpty()) {
_calculated.update { null }
- } else {
- _calculated.update {
- evaluationResult
- .setMinimumRequiredScale(_userPrefs.value.digitsPrecision)
- .trimZeros()
- .toStringWith(_userPrefs.value.outputFormat)
- }
+ _result.update { ConversionResult.Default("") }
+ return@launch
}
- // Now we just convert.
- // We can use evaluation result here, input is valid.
- val conversionResult: BigDecimal = unitFrom.convert(
- unitTo,
- evaluationResult,
- _userPrefs.value.digitsPrecision
- )
-
- // Converted
- _result.update { conversionResult.toStringWith(_userPrefs.value.outputFormat) }
- }
-
- private fun setInputSymbols(symbol: String, add: Boolean = true) {
- if (add) {
- _input.update { it + symbol }
- } else {
- // We don't need previous input, clear entirely
- _latestInputStack.clear()
- _input.update { symbol }
+ val evaluationResult = try {
+ Expression(input)
+ .calculate()
+ .also {
+ if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()
+ }
+ .setMinimumRequiredScale(_userPrefs.value.digitsPrecision)
+ .trimZeros()
+ } catch (e: ExpressionException.DivideByZero) {
+ _calculated.update { null }
+ return@launch
+ } catch (e: Exception) {
+ return@launch
+ }
+
+ _calculated.update {
+ if (input.isExpression()) evaluationResult.toStringWith(_userPrefs.value.outputFormat)
+ else null
+ }
+
+ val conversionResult = unitFrom.convert(
+ unitTo = unitTo,
+ value = evaluationResult,
+ scale = _userPrefs.value.digitsPrecision
+ ).toStringWith(_userPrefs.value.outputFormat)
+
+ _result.update {
+ if ((unitFrom.group == UnitGroup.TIME) and (_userPrefs.value.unitConverterFormatTime))
+ ConversionResult.Time(conversionResult)
+ else
+ ConversionResult.Default(conversionResult)
}
- _latestInputStack.add(symbol)
}
private fun incrementCounter(unit: AbstractUnit) {
@@ -502,20 +285,20 @@ class ConverterViewModel @Inject constructor(
}
}
- private fun updateCurrenciesRatesIfNeeded() {
+ fun updateCurrenciesRatesIfNeeded() {
viewModelScope.launch(Dispatchers.IO) {
_showError.update { false }
_showLoading.update { false }
+
// Units are still loading, don't convert anything yet
- val unitFrom = _unitFrom.value ?: return@launch
if (_unitFrom.value?.group != UnitGroup.CURRENCY) return@launch
+ val unitFrom = _unitFrom.value ?: return@launch
// Starting to load stuff
_showLoading.update { true }
try {
- val pairs: CurrencyUnitResponse =
- CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId)
- allUnitsRepository.updateBasicUnitsForCurrencies(pairs.currency)
+ allUnitsRepository.updateBasicUnitsForCurrencies(unitFrom)
+ convertAsExpression()
} catch (e: Exception) {
// Dangerous and stupid, but who cares
_showError.update { true }
@@ -539,7 +322,7 @@ class ConverterViewModel @Inject constructor(
}
private fun startObserving() {
- viewModelScope.launch(Dispatchers.Default) {
+ viewModelScope.launch {
merge(_input, _unitFrom, _unitTo, _showLoading, _userPrefs).collectLatest {
convertInput()
}
@@ -548,7 +331,7 @@ class ConverterViewModel @Inject constructor(
private fun loadInitialUnitPair() {
viewModelScope.launch(Dispatchers.IO) {
- val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first()
+ val initialUserPrefs = userPrefsRepository.mainPreferencesFlow.first()
// First we load latest pair of units
_unitFrom.update {
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt
index e999fe20..9d54af01 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.sadellie.unitto.core.base.Token
-import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
import com.sadellie.unitto.core.ui.common.KeyboardButtonLight
@@ -77,11 +76,12 @@ internal fun Keyboard(
deleteDigit: () -> Unit = {},
clearInput: () -> Unit = {},
converterMode: ConverterMode,
- allowVibration: Boolean
+ allowVibration: Boolean,
+ fractional: String,
) {
Crossfade(converterMode, modifier = modifier) {
when (it) {
- ConverterMode.DEFAULT -> DefaultKeyboard(addDigit, clearInput, deleteDigit, allowVibration)
+ ConverterMode.DEFAULT -> DefaultKeyboard(addDigit, clearInput, deleteDigit, allowVibration, fractional)
ConverterMode.BASE -> BaseKeyboard(addDigit, clearInput, deleteDigit, allowVibration)
}
}
@@ -92,9 +92,10 @@ private fun DefaultKeyboard(
addDigit: (String) -> Unit,
clearInput: () -> Unit,
deleteDigit: () -> Unit,
- allowVibration: Boolean
+ allowVibration: Boolean,
+ fractional: String,
) {
- val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma }
+ val fractionalIcon = remember { if (fractional == Token.Digit.dot) UnittoIcons.Dot else UnittoIcons.Comma }
ColumnWithConstraints {
// Button modifier
val bModifier = Modifier
@@ -104,34 +105,34 @@ private fun DefaultKeyboard(
val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f)
val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f)
Row(cModifier, horizontalArrangement) {
- KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.leftBracket) }
- KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.rightBracket) }
- KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.exponent) }
- KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.sqrt) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.Operator.leftBracket) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.Operator.rightBracket) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.Operator.power) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.Operator.sqrt) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) }
- KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.divide) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token.Digit._7) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token.Digit._8) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token.Digit._9) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.Operator.divide) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) }
- KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.multiply) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token.Digit._4) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token.Digit._5) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token.Digit._6) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.Operator.multiply) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) }
- KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.minus) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token.Digit._1) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token.Digit._2) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token.Digit._3) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.Operator.minus) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) }
- KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.dot) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token.Digit._0) }
+ KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.Digit.dot) }
KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() }
- KeyboardButtonFilled(bModifier, UnittoIcons.Plus, allowVibration) { addDigit(Token.plus) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.Plus, allowVibration) { addDigit(Token.Operator.plus) }
}
}
}
@@ -152,32 +153,32 @@ private fun BaseKeyboard(
val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f)
val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f)
Row(cModifier, horizontalArrangement) {
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.baseA) }
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.baseB) }
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.baseC) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.Letter._A) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.Letter._B) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.Letter._C) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.baseD) }
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.baseE) }
- KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.baseF) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.Letter._D) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.Letter._E) }
+ KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.Letter._F) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token.Digit._7) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token.Digit._8) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token.Digit._9) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token.Digit._4) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token.Digit._5) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token.Digit._6) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) }
- KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token.Digit._1) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token.Digit._2) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token.Digit._3) }
}
Row(cModifier, horizontalArrangement) {
- KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) }
+ KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token.Digit._0) }
KeyboardButtonLight(
Modifier.fillMaxSize().weight(2f).padding(it.maxWidth * 0.015f, it.maxHeight * 0.008f), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() }
}
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt
index e344d063..e69de29b 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt
@@ -1,174 +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.feature.converter.components
-
-import android.widget.Toast
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.expandHorizontally
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.animation.with
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.R
-import com.sadellie.unitto.core.ui.common.textfield.InputTextField
-import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
-
-/**
- * Component for input and output
- *
- * @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 textToCopy Text that will be copied to clipboard when long-clicking.
- */
-@Composable
-internal fun MyTextField(
- modifier: Modifier,
- primaryText: @Composable () -> String,
- secondaryText: String?,
- helperText: String,
- textToCopy: String,
- onClick: () -> Unit = {},
-) {
- val clipboardManager = LocalClipboardManager.current
- val mc = LocalContext.current
- val textToShow: String = primaryText()
- val copiedText: String =
- stringResource(R.string.copied, textToCopy)
-
- Column(
- modifier = Modifier
- .combinedClickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(),
- onClick = onClick,
- onLongClick = {
- clipboardManager.setText(AnnotatedString(secondaryText ?: textToShow))
- Toast
- .makeText(mc, copiedText, Toast.LENGTH_SHORT)
- .show()
- }
- )
- ) {
- LazyRow(
- modifier = modifier
- .wrapContentHeight()
- .weight(2f),
- reverseLayout = true,
- horizontalArrangement = Arrangement.End,
- contentPadding = PaddingValues(horizontal = 8.dp)
- ) {
- item {
- AnimatedContent(
- targetState = textToShow,
- transitionSpec = {
- // Enter animation
- (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn()
- // Exit animation
- with fadeOut())
- .using(SizeTransform(clip = false))
- }
- ) {
- InputTextField(
- modifier = Modifier.fillMaxWidth(),
- value = it.take(1000),
- textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End)
- )
- }
- }
- }
-
- AnimatedVisibility(
- modifier = Modifier.weight(1f),
- visible = !secondaryText.isNullOrEmpty(),
- enter = expandVertically(),
- exit = shrinkVertically()
- ) {
- LazyRow(
- modifier = modifier
- .wrapContentHeight(),
- reverseLayout = true,
- horizontalArrangement = Arrangement.End,
- contentPadding = PaddingValues(horizontal = 8.dp)
- ) {
- item {
- AnimatedContent(
- targetState = secondaryText,
- transitionSpec = {
- // Enter animation
- (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn()
- // Exit animation
- with fadeOut())
- .using(SizeTransform(clip = false))
- }
- ) {
- InputTextField(
- modifier = Modifier.fillMaxWidth(),
- value = it?.take(1000) ?: "",
- textStyle = NumbersTextStyleDisplayLarge.copy(
- textAlign = TextAlign.End,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
- ),
- minRatio = 0.7f
- )
- }
- }
- }
- }
-
- AnimatedContent(
- modifier = Modifier
- .align(Alignment.End)
- .padding(horizontal = 8.dp)
- .weight(1f),
- targetState = helperText
- ) {
- Text(
- text = it,
- style = MaterialTheme.typography.bodyMedium
- )
- }
- }
-}
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt
index 39f1af65..d2cc56d5 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt
@@ -20,6 +20,7 @@ package com.sadellie.unitto.feature.converter.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.Crossfade
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
@@ -29,7 +30,7 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
-import androidx.compose.animation.with
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -50,13 +51,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
-import com.sadellie.unitto.core.ui.Formatter
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.base.Token
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
-import com.sadellie.unitto.core.ui.common.textfield.InputTextField
+import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField
+import com.sadellie.unitto.core.ui.common.textfield.formatExpression
+import com.sadellie.unitto.core.ui.common.textfield.formatTime
import com.sadellie.unitto.data.model.AbstractUnit
-import com.sadellie.unitto.data.model.UnitGroup
+import com.sadellie.unitto.feature.converter.ConversionResult
import com.sadellie.unitto.feature.converter.ConverterMode
/**
@@ -70,29 +77,28 @@ import com.sadellie.unitto.feature.converter.ConverterMode
* @param outputValue Current output value (like big decimal).
* @param unitFrom [AbstractUnit] on the left.
* @param unitTo [AbstractUnit] on the right.
- * @param networkLoading Are we loading data from network? Shows loading text in TextFields.
- * @param networkError Did we got errors while trying to get data from network.
* @param navigateToLeftScreen Function that is called when clicking left unit selection button.
* @param navigateToRightScreen Function that is called when clicking right unit selection button.
* @param swapUnits Method to swap units.
* @param converterMode [ConverterMode.BASE] doesn't use formatting for input/output.
- * @param formatTime If True will use [Formatter.formatTime].
*/
@Composable
internal fun TopScreenPart(
modifier: Modifier,
- inputValue: String,
+ inputValue: TextFieldValue,
calculatedValue: String?,
- outputValue: String,
+ outputValue: ConversionResult,
unitFrom: AbstractUnit?,
unitTo: AbstractUnit?,
- networkLoading: Boolean,
- networkError: Boolean,
navigateToLeftScreen: (String) -> Unit,
- navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit,
+ navigateToRightScreen: (unitFrom: String, unitTo: String, input: String?) -> Unit,
swapUnits: () -> Unit,
converterMode: ConverterMode,
- formatTime: Boolean,
+ onCursorChange: (TextRange) -> Unit,
+ cutCallback: () -> Unit,
+ pasteCallback: (String) -> Unit,
+ formatterSymbols: FormatterSymbols,
+ onErrorClick: () -> Unit,
) {
var swapped by remember { mutableStateOf(false) }
val swapButtonRotation: Float by animateFloatAsState(
@@ -104,24 +110,51 @@ internal fun TopScreenPart(
ColumnWithConstraints(
modifier = modifier,
) {
- InputTextField(
- modifier = Modifier.weight(2f),
- value = when (converterMode) {
- ConverterMode.BASE -> inputValue.uppercase()
- else -> Formatter.format(inputValue)
- },
- minRatio = 0.7f
- )
+ Crossfade(modifier = Modifier.weight(2f), targetState = converterMode) { mode ->
+ if (mode == ConverterMode.BASE) {
+ UnformattedTextField(
+ modifier = Modifier,
+ value = inputValue,
+ onCursorChange = onCursorChange,
+ minRatio = 0.7f,
+ cutCallback = cutCallback,
+ pasteCallback = pasteCallback,
+ placeholder = Token.Digit._0
+ )
+ } else {
+ ExpressionTextField(
+ modifier = Modifier,
+ value = inputValue,
+ onCursorChange = onCursorChange,
+ formatterSymbols = formatterSymbols,
+ minRatio = 0.7f,
+ cutCallback = cutCallback,
+ pasteCallback = pasteCallback,
+ placeholder = Token.Digit._0
+ )
+ }
+ }
AnimatedVisibility(
visible = !calculatedValue.isNullOrEmpty(),
modifier = Modifier.weight(1f),
enter = expandVertically(clip = false),
exit = shrinkVertically(clip = false)
) {
- InputTextField(
- value = calculatedValue?.let { value -> Formatter.format(value) } ?: "",
- minRatio = 0.7f,
- textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
+ var calculatedTextFieldValue by remember(calculatedValue) {
+ mutableStateOf(
+ TextFieldValue(calculatedValue?.formatExpression(formatterSymbols) ?: "")
+ )
+ }
+ ExpressionTextField(
+ modifier = Modifier,
+ value = calculatedTextFieldValue,
+ onCursorChange = { newSelection ->
+ calculatedTextFieldValue =
+ calculatedTextFieldValue.copy(selection = newSelection)
+ },
+ formatterSymbols = formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
+ minRatio = 0.7f
)
}
AnimatedContent(
@@ -130,8 +163,7 @@ internal fun TopScreenPart(
transitionSpec = {
// Enter animation
(expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn()
- // Exit animation
- with fadeOut())
+ togetherWith fadeOut())
.using(SizeTransform(clip = false))
}
) { value ->
@@ -141,32 +173,94 @@ internal fun TopScreenPart(
)
}
- InputTextField(
- modifier = Modifier
- .weight(2f),
- value = when {
- networkLoading -> stringResource(R.string.loading_label)
- networkError -> stringResource(R.string.error_label)
- converterMode == ConverterMode.BASE -> outputValue.uppercase()
- formatTime and (unitTo?.group == UnitGroup.TIME) -> {
- Formatter.formatTime(
- context = mContext,
- input = calculatedValue ?: inputValue,
- basicUnit = unitFrom?.basicUnit
+ when (outputValue) {
+ is ConversionResult.Default -> {
+ var outputTextFieldValue: TextFieldValue by remember(outputValue) {
+ mutableStateOf(TextFieldValue(outputValue.result))
+ }
+ ExpressionTextField(
+ modifier = Modifier.weight(2f),
+ value = outputTextFieldValue,
+ onCursorChange = { newSelection ->
+ outputTextFieldValue = outputTextFieldValue.copy(selection = newSelection)
+ },
+ formatterSymbols = formatterSymbols,
+ readOnly = true,
+ minRatio = 0.7f
+ )
+ }
+
+ is ConversionResult.Time -> {
+ var outputTextFieldValue: TextFieldValue by remember(outputValue) {
+ mutableStateOf(
+ TextFieldValue(
+ outputValue.result
+ .formatTime(mContext, unitTo?.basicUnit, formatterSymbols)
+ )
)
}
- else -> Formatter.format(outputValue)
- },
- minRatio = 0.7f,
- )
+ UnformattedTextField(
+ modifier = Modifier.weight(2f),
+ value = outputTextFieldValue,
+ onCursorChange = { newSelection ->
+ outputTextFieldValue = outputTextFieldValue.copy(selection = newSelection)
+ },
+ minRatio = 0.7f,
+ readOnly = true
+ )
+ }
+
+ is ConversionResult.NumberBase -> {
+ var outputTextFieldValue: TextFieldValue by remember(outputValue) {
+ mutableStateOf(TextFieldValue(outputValue.result.uppercase()))
+ }
+ UnformattedTextField(
+ modifier = Modifier.weight(2f),
+ value = outputTextFieldValue,
+ onCursorChange = { newSelection ->
+ outputTextFieldValue = outputTextFieldValue.copy(selection = newSelection)
+ },
+ minRatio = 0.7f,
+ readOnly = true
+ )
+ }
+
+ is ConversionResult.Loading -> {
+ UnformattedTextField(
+ modifier = Modifier.weight(2f),
+ value = TextFieldValue(stringResource(R.string.loading_label)),
+ onCursorChange = {},
+ minRatio = 0.7f,
+ readOnly = true
+ )
+ }
+
+ is ConversionResult.Error -> {
+ UnformattedTextField(
+ modifier = Modifier.weight(2f),
+ value = TextFieldValue(stringResource(R.string.error_label)),
+ onCursorChange = { onErrorClick() },
+ minRatio = 0.7f,
+ readOnly = true,
+ textColor = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+
+ val supportLabelTo = when {
+ outputValue is ConversionResult.Error -> R.string.try_again_label
+ (unitTo?.shortName != null) -> unitTo.shortName
+ else -> R.string.loading_label
+ }
+
AnimatedContent(
modifier = Modifier.fillMaxWidth(),
- targetState = stringResource(unitTo?.shortName ?: R.string.loading_label),
+ targetState = stringResource(supportLabelTo),
transitionSpec = {
// Enter animation
(expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn()
// Exit animation
- with fadeOut())
+ togetherWith fadeOut())
.using(SizeTransform(clip = false))
}
) { value ->
@@ -206,10 +300,16 @@ internal fun TopScreenPart(
onClick = {
if (unitTo == null) return@UnitSelectionButton
if (unitFrom == null) return@UnitSelectionButton
+
+ val input = when (outputValue) {
+ is ConversionResult.Error, ConversionResult.Loading -> null
+ else -> calculatedValue ?: inputValue.text
+ }
+
navigateToRightScreen(
unitFrom.unitId,
unitTo.unitId,
- calculatedValue ?: inputValue
+ input?.ifEmpty { "0" }
)
},
label = unitTo?.displayName ?: R.string.loading_label,
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitSelectionButton.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitSelectionButton.kt
index 52cf5912..40097f47 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitSelectionButton.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/UnitSelectionButton.kt
@@ -18,32 +18,25 @@
package com.sadellie.unitto.feature.converter.components
+import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.animateIntAsState
-import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
-import androidx.compose.animation.with
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.UnittoButton
/**
* Button to select a unit
@@ -52,39 +45,29 @@ import com.sadellie.unitto.core.ui.R
* @param onClick Function to call when button is clicked (navigate to a unit selection screen)
* @param label Text on button
*/
+@SuppressLint("UnusedContentLambdaTargetStateParameter")
@Composable
internal fun UnitSelectionButton(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
label: Int?,
) {
- val interactionSource = remember { MutableInteractionSource() }
- val isPressed by interactionSource.collectIsPressedAsState()
- val cornerRadius: Int by animateIntAsState(
- targetValue = if (isPressed) 30 else 50,
- animationSpec = tween(easing = FastOutSlowInEasing),
- )
-
- Button(
+ UnittoButton(
modifier = modifier,
- shape = RoundedCornerShape(cornerRadius),
onClick = onClick,
enabled = label != null,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer
- ),
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
contentPadding = PaddingValues(vertical = 16.dp, horizontal = 8.dp),
- interactionSource = interactionSource
) {
AnimatedContent(
targetState = label ?: 0,
transitionSpec = {
if (targetState > initialState) {
- slideInVertically { height -> height } + fadeIn() with
- slideOutVertically { height -> -height } + fadeOut()
+ (slideInVertically { height -> height } + fadeIn()) togetherWith
+ slideOutVertically { height -> -height } + fadeOut()
} else {
- slideInVertically { height -> -height } + fadeIn() with
- slideOutVertically { height -> height } + fadeOut()
+ (slideInVertically { height -> -height } + fadeIn()) togetherWith
+ slideOutVertically { height -> height } + fadeOut()
}.using(
SizeTransform(clip = false)
)
diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt
index aed19e68..ca10a128 100644
--- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt
+++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt
@@ -28,7 +28,7 @@ private val converterRoute: String by lazy { TopLevelDestinations.Converter.rout
fun NavGraphBuilder.converterScreen(
navigateToLeftScreen: (String) -> Unit,
- navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit,
+ navigateToRightScreen: (unitFrom: String, unitTo: String, input: String?) -> Unit,
navigateToSettings: () -> Unit,
navigateToMenu: () -> Unit,
viewModel: ConverterViewModel
diff --git a/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/ConverterViewModelTest.kt b/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/ConverterViewModelTest.kt
deleted file mode 100644
index 013d6ae4..00000000
--- a/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/ConverterViewModelTest.kt
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2022-2023 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.feature.converter
-
-import androidx.room.Room
-import com.sadellie.unitto.core.base.Token
-import com.sadellie.unitto.data.database.UnitsRepository
-import com.sadellie.unitto.data.database.UnittoDatabase
-import com.sadellie.unitto.data.units.AllUnitsRepository
-import com.sadellie.unitto.data.userprefs.DataStoreModule
-import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
-import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.RuntimeEnvironment
-import org.robolectric.annotation.Config
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@Config(manifest = Config.NONE)
-@RunWith(RobolectricTestRunner::class)
-class ConverterViewModelTest {
-
- @ExperimentalCoroutinesApi
- @get:Rule
- val coroutineTestRule = CoroutineTestRule()
-
- private lateinit var viewModel: ConverterViewModel
- private val allUnitsRepository = AllUnitsRepository()
- private val database = Room.inMemoryDatabaseBuilder(
- RuntimeEnvironment.getApplication(),
- UnittoDatabase::class.java
- ).build()
-
- @Before
- fun setUp() {
- viewModel = ConverterViewModel(
- userPrefsRepository = UserPreferencesRepository(
- DataStoreModule()
- .provideUserPreferencesDataStore(
- RuntimeEnvironment.getApplication()
- )
- ),
- unitRepository = UnitsRepository(
- database.unitsDao()
- ),
- allUnitsRepository = allUnitsRepository
- )
- }
-
- @After
- fun tearDown() {
- database.close()
- }
-
- @Test
- fun `test 0`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
-
- inputOutputTest("0", "0")
- inputOutputTest("123000", "123000")
- inputOutputTest("123.000", "123.000")
- inputOutputTest("-000", "-0")
- inputOutputTest("12+000", "12+0")
- inputOutputTest("â000", "â0")
- inputOutputTest("(000", "(0")
- inputOutputTest("(1+12)000", "(1+12)*0")
- inputOutputTest("(1.002+120)000", "(1.002+120)*0")
-
- collectJob.cancel()
- }
-
- @Test
- fun `test digits from 1 to 9`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("123456789", "123456789")
- inputOutputTest("(1+1)111", "(1+1)*111")
- collectJob.cancel()
- }
-
- @Test
- fun `test plus, divide, multiply and exponent operators`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("0+++", "0")
- inputOutputTest("123+++", "123+")
- inputOutputTest("1-***", "1*")
- inputOutputTest("1/-+++", "1+")
- inputOutputTest("0^^^", "0")
- inputOutputTest("12^^^", "12^")
- inputOutputTest("(^^^", "(")
- inputOutputTest("(8+9)^^^", "(8+9)^")
- collectJob.cancel()
- }
-
- @Test
- fun `test dot`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("0...", "0.")
- inputOutputTest("1...", "1.")
- inputOutputTest("1+...", "1+.")
- inputOutputTest("â...", "â.")
- inputOutputTest("â21...", "â21.")
- inputOutputTest("â21+1.01-.23...", "â21+1.01-.23")
- collectJob.cancel()
- }
-
- @Test
- fun `test minus`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("0---", "-")
- inputOutputTest("12---", "12-")
- inputOutputTest("12+---", "12-")
- inputOutputTest("12/---", "12/-")
- inputOutputTest("â---", "â-")
- inputOutputTest("â///", "â")
- inputOutputTest("12^----", "12^-")
- collectJob.cancel()
- }
-
- @Test
- fun `test brackets`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("0)))", "0")
- inputOutputTest("0(((", "(((")
- inputOutputTest("â(10+2)(", "â(10+2)*(")
- inputOutputTest("â(10+2./(", "â(10+2./(")
- inputOutputTest("0()()))((", "((((")
- inputOutputTest("â(10+2)^(", "â(10+2)^(")
- collectJob.cancel()
- }
-
- @Test
- fun `test square root`() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- inputOutputTest("0âââ", "âââ")
- inputOutputTest("123âââ", "123*âââ")
- collectJob.cancel()
- }
-
- @Test
- fun deleteSymbolTest() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
-
- listOf(
- Token._1, Token._2, Token._3, Token._4, Token._5,
- Token._6, Token._7, Token._8, Token._9, Token._0,
- Token.dot, Token.comma, Token.leftBracket, Token.rightBracket,
- Token.plus, Token.minus, Token.divide, Token.multiply,
- Token.exponent, Token.sqrt
- ).forEach {
- // We enter one symbol and delete it, should be default as a result
- viewModel.processInput(it)
- viewModel.deleteDigit()
- assertEquals("0", viewModel.uiStateFlow.value.inputValue)
- }
- viewModel.clearInput()
-
- // This should not delete default input (0)
- viewModel.deleteDigit()
-
- // Now we check that we can delete multiple values
- viewModel.processInput(Token._3)
- viewModel.processInput(Token.sqrt)
- viewModel.processInput(Token._9)
- viewModel.deleteDigit()
- assertEquals("3*â", viewModel.uiStateFlow.value.inputValue)
-
- collectJob.cancel()
- }
-
- @Test
- fun clearInputTest() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
-
- viewModel.processInput(Token._3)
- viewModel.clearInput()
- assertEquals(null, viewModel.uiStateFlow.value.calculatedValue)
-
- viewModel.processInput(Token._3)
- viewModel.processInput(Token.multiply)
- viewModel.clearInput()
- assertEquals(null, viewModel.uiStateFlow.value.calculatedValue)
-
- collectJob.cancel()
- }
-
- @Test
- fun swapUnitsTest() = runTest {
- val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
- viewModel.uiStateFlow.collect()
- }
- val initialFrom = viewModel.uiStateFlow.value.unitFrom?.unitId
- val initialTo = viewModel.uiStateFlow.value.unitTo?.unitId
-
- viewModel.swapUnits()
- assertEquals(initialTo, viewModel.uiStateFlow.value.unitFrom?.unitId)
- assertEquals(initialFrom, viewModel.uiStateFlow.value.unitTo?.unitId)
-
- collectJob.cancel()
- }
-
- /**
- * Takes [input] sequence as a single string (e.g. "123-23") and compares it with [output].
- */
- private fun inputOutputTest(input: String, output: String) {
- // Enter everything
- input.forEach {
- viewModel.processInput(it.toString())
- }
- assertEquals(output, viewModel.uiStateFlow.value.inputValue)
- viewModel.clearInput()
- }
-}
diff --git a/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/CoroutinesTestUtils.kt b/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/CoroutinesTestUtils.kt
deleted file mode 100644
index 68734339..00000000
--- a/feature/converter/src/test/java/com/sadellie/unitto/feature/converter/CoroutinesTestUtils.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Unitto is a unit converter for Android
- * Copyright (c) 2022-2023 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.feature.converter
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestDispatcher
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
-import org.junit.rules.TestWatcher
-import org.junit.runner.Description
-
-@ExperimentalCoroutinesApi
-class CoroutineTestRule(private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()) :
- TestWatcher() {
-
- override fun starting(description: Description) {
- super.starting(description)
- Dispatchers.setMain(dispatcher)
- }
-
- override fun finished(description: Description) {
- super.finished(description)
- Dispatchers.resetMain()
- }
-
-}
diff --git a/feature/datedifference/.gitignore b/feature/datedifference/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/datedifference/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/datedifference/build.gradle.kts b/feature/datedifference/build.gradle.kts
new file mode 100644
index 00000000..57b8de18
--- /dev/null
+++ b/feature/datedifference/build.gradle.kts
@@ -0,0 +1,32 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+plugins {
+ id("unitto.library")
+ id("unitto.library.compose")
+ id("unitto.library.feature")
+ id("unitto.android.hilt")
+}
+
+android {
+ namespace = "com.sadellie.unitto.feature.datedifference"
+}
+
+dependencies {
+ testImplementation(libs.junit)
+}
diff --git a/feature/datedifference/consumer-rules.pro b/feature/datedifference/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/core/base/src/main/res/mipmap-anydpi-v26/ic_launcher_icon_round.xml b/feature/datedifference/src/main/AndroidManifest.xml
similarity index 71%
rename from core/base/src/main/res/mipmap-anydpi-v26/ic_launcher_icon_round.xml
rename to feature/datedifference/src/main/AndroidManifest.xml
index cfabe93f..7bdbce91 100644
--- a/core/base/src/main/res/mipmap-anydpi-v26/ic_launcher_icon_round.xml
+++ b/feature/datedifference/src/main/AndroidManifest.xml
@@ -17,8 +17,6 @@
~ along with this program. If not, see .
-->
-
-
-
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifference.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifference.kt
new file mode 100644
index 00000000..87c748a4
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifference.kt
@@ -0,0 +1,82 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference
+
+import java.time.LocalDateTime
+import java.time.temporal.ChronoUnit
+
+internal sealed class DateDifference(
+ open val years: Long = 0,
+ open val months: Long = 0,
+ open val days: Long = 0,
+ open val hours: Long = 0,
+ open val minutes: Long = 0,
+) {
+ data class Default(
+ override val years: Long = 0,
+ override val months: Long = 0,
+ override val days: Long = 0,
+ override val hours: Long = 0,
+ override val minutes: Long = 0,
+ ) : DateDifference(
+ years = years,
+ months = months,
+ days = days,
+ hours = hours,
+ minutes = minutes,
+ )
+
+ object Zero : DateDifference()
+}
+
+// https://stackoverflow.com/a/25760725
+internal infix operator fun LocalDateTime.minus(localDateTime: LocalDateTime): DateDifference {
+ if (this == localDateTime) return DateDifference.Zero
+
+ var fromDateTime: LocalDateTime = this
+ var toDateTime: LocalDateTime = localDateTime
+
+ // Swap to avoid negative
+ if (this > localDateTime) {
+ fromDateTime = localDateTime
+ toDateTime = this
+ }
+
+ var tempDateTime = LocalDateTime.from(fromDateTime)
+
+ val years = tempDateTime.until(toDateTime, ChronoUnit.YEARS)
+
+ tempDateTime = tempDateTime.plusYears(years)
+ val months = tempDateTime.until(toDateTime, ChronoUnit.MONTHS)
+
+ tempDateTime = tempDateTime.plusMonths(months)
+ val days = tempDateTime.until(toDateTime, ChronoUnit.DAYS)
+
+ tempDateTime = tempDateTime.plusDays(days)
+ val hours = tempDateTime.until(toDateTime, ChronoUnit.HOURS)
+
+ tempDateTime = tempDateTime.plusHours(hours)
+ val minutes = tempDateTime.until(toDateTime, ChronoUnit.MINUTES)
+
+ if (listOf(years, months, days, hours, minutes).sum() == 0L) return DateDifference.Zero
+
+ return DateDifference.Default(
+ years = years, months = months, days = days, hours = hours, minutes = minutes
+ )
+}
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceScreen.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceScreen.kt
new file mode 100644
index 00000000..f4cd5377
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceScreen.kt
@@ -0,0 +1,231 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference
+
+import android.content.res.Configuration
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.DatePickerDialog
+import com.sadellie.unitto.core.ui.common.MenuButton
+import com.sadellie.unitto.core.ui.common.SettingsButton
+import com.sadellie.unitto.core.ui.common.TimePickerDialog
+import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
+import com.sadellie.unitto.feature.datedifference.components.DateTimeResultBlock
+import com.sadellie.unitto.feature.datedifference.components.DateTimeSelectorBlock
+import java.time.LocalDateTime
+
+
+@Composable
+internal fun DateDifferenceRoute(
+ viewModel: DateDifferenceViewModel = hiltViewModel(),
+ navigateToMenu: () -> Unit,
+ navigateToSettings: () -> Unit,
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle()
+ DateDifferenceScreen(
+ navigateToMenu = navigateToMenu,
+ navigateToSettings = navigateToSettings,
+ uiState = uiState.value,
+ updateStart = viewModel::setStartTime,
+ updateEnd = viewModel::setEndTime
+ )
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+internal fun DateDifferenceScreen(
+ navigateToMenu: () -> Unit,
+ navigateToSettings: () -> Unit,
+ updateStart: (LocalDateTime) -> Unit,
+ updateEnd: (LocalDateTime) -> Unit,
+ uiState: UIState,
+) {
+ var dialogState by remember { mutableStateOf(DialogState.NONE) }
+ val isVertical = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
+
+ UnittoScreenWithTopBar(
+ title = { Text(stringResource(R.string.date_difference)) },
+ navigationIcon = { MenuButton(navigateToMenu) },
+ actions = {
+ SettingsButton(navigateToSettings)
+ }
+ ) { paddingValues ->
+ FlowRow(
+ modifier = Modifier
+ .padding(paddingValues)
+ .padding(horizontal = 16.dp),
+ maxItemsInEachRow = 2,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ DateTimeSelectorBlock(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ title = stringResource(R.string.date_difference_start),
+ onClick = { dialogState = DialogState.FROM },
+ onTimeClick = { dialogState = DialogState.FROM_TIME },
+ onDateClick = { dialogState = DialogState.FROM_DATE },
+ dateTime = uiState.start
+ )
+
+ DateTimeSelectorBlock(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ title = stringResource(R.string.date_difference_end),
+ onClick = { dialogState = DialogState.TO },
+ onTimeClick = { dialogState = DialogState.TO_TIME },
+ onDateClick = { dialogState = DialogState.TO_DATE },
+ dateTime = uiState.end
+ )
+
+ AnimatedVisibility(
+ visible = uiState.result is DateDifference.Default,
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ DateTimeResultBlock(
+ modifier = Modifier
+ .weight(2f)
+ .fillMaxWidth(),
+ dateDifference = uiState.result
+ )
+ }
+ }
+ }
+
+ fun resetDialog() {
+ dialogState = DialogState.NONE
+ }
+
+ when (dialogState) {
+ DialogState.FROM -> {
+ TimePickerDialog(
+ localDateTime = uiState.start,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateStart(it)
+ dialogState = DialogState.FROM_DATE
+ },
+ confirmLabel = stringResource(R.string.next_label),
+ vertical = isVertical,
+ )
+ }
+
+ DialogState.FROM_TIME -> {
+ TimePickerDialog(
+ localDateTime = uiState.start,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateStart(it)
+ resetDialog()
+ },
+ vertical = isVertical,
+ )
+ }
+
+ DialogState.FROM_DATE -> {
+ DatePickerDialog(
+ localDateTime = uiState.start,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateStart(it)
+ resetDialog()
+ }
+ )
+ }
+
+ DialogState.TO -> {
+ TimePickerDialog(
+ localDateTime = uiState.end,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateEnd(it)
+ dialogState = DialogState.TO_DATE
+ },
+ confirmLabel = stringResource(R.string.next_label),
+ vertical = isVertical,
+ )
+ }
+
+ DialogState.TO_TIME -> {
+ TimePickerDialog(
+ localDateTime = uiState.end,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateEnd(it)
+ resetDialog()
+ },
+ vertical = isVertical,
+ )
+ }
+
+ DialogState.TO_DATE -> {
+ DatePickerDialog(
+ localDateTime = uiState.end,
+ onDismiss = ::resetDialog,
+ onConfirm = {
+ updateEnd(it)
+ resetDialog()
+ }
+ )
+ }
+
+ else -> {}
+ }
+}
+
+private enum class DialogState {
+ NONE, FROM, FROM_TIME, FROM_DATE, TO, TO_TIME, TO_DATE
+}
+
+@Preview
+@Composable
+private fun DateDifferenceScreenPreview() {
+ DateDifferenceScreen(
+ navigateToMenu = {},
+ navigateToSettings = {},
+ updateStart = {},
+ updateEnd = {},
+ uiState = UIState(
+ result = DateDifference.Default(4, 1, 2, 3, 4)
+ )
+ )
+}
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceViewModel.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceViewModel.kt
new file mode 100644
index 00000000..1d151afe
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/DateDifferenceViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.time.LocalDateTime
+import javax.inject.Inject
+
+@HiltViewModel
+internal class DateDifferenceViewModel @Inject constructor() : ViewModel() {
+ private val _start = MutableStateFlow(LocalDateTime.now())
+ private val _end = MutableStateFlow(LocalDateTime.now())
+ private val _result = MutableStateFlow(DateDifference.Zero)
+
+ val uiState = combine(_start, _end, _result) { start, end, result ->
+ return@combine UIState(start, end, result)
+ }
+ .stateIn(
+ viewModelScope, SharingStarted.WhileSubscribed(5000L), UIState()
+ )
+
+ fun setStartTime(newTime: LocalDateTime) = _start.update { newTime }
+
+ fun setEndTime(newTime: LocalDateTime) = _end.update { newTime }
+
+ init {
+ viewModelScope.launch(Dispatchers.Default) {
+ merge(_start, _end).collectLatest {
+ val difference = _start.value - _end.value
+ _result.update { difference }
+ }
+ }
+ }
+}
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/UIState.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/UIState.kt
new file mode 100644
index 00000000..7dd57728
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/UIState.kt
@@ -0,0 +1,27 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference
+
+import java.time.LocalDateTime
+
+internal data class UIState(
+ val start: LocalDateTime = LocalDateTime.now(),
+ val end: LocalDateTime = LocalDateTime.now(),
+ val result: DateDifference = DateDifference.Zero
+)
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeResultBlock.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeResultBlock.kt
new file mode 100644
index 00000000..2b841809
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeResultBlock.kt
@@ -0,0 +1,130 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference.components
+
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.squashable
+import com.sadellie.unitto.feature.datedifference.DateDifference
+
+@Composable
+internal fun DateTimeResultBlock(
+ modifier: Modifier = Modifier,
+ dateDifference: DateDifference
+) {
+ val clipboardManager = LocalClipboardManager.current
+
+ val years = dateDifference.years.formatDateTimeValue(R.string.date_difference_years)
+ val months = dateDifference.months.formatDateTimeValue(R.string.date_difference_months)
+ val days = dateDifference.days.formatDateTimeValue(R.string.date_difference_days)
+ val hours = dateDifference.hours.formatDateTimeValue(R.string.date_difference_hours)
+ val minutes = dateDifference.minutes.formatDateTimeValue(R.string.date_difference_minutes)
+
+ val texts = listOf(years, months, days, hours, minutes)
+
+ Column(
+ modifier = modifier
+ .squashable(
+ onClick = {},
+ interactionSource = remember { MutableInteractionSource() },
+ cornerRadiusRange = 8.dp..32.dp,
+ )
+ .background(MaterialTheme.colorScheme.tertiaryContainer)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ stringResource(R.string.date_difference_result),
+ style = MaterialTheme.typography.labelMedium
+ )
+ IconButton(
+ onClick = {
+ clipboardManager.setText(
+ AnnotatedString(texts.filter { it.isNotEmpty() }.joinToString(" "))
+ )
+ }
+ ) {
+ Icon(Icons.Default.ContentCopy, null)
+ }
+ }
+
+ texts.forEach {
+ AnimatedVisibility(
+ visible = it.isNotEmpty(),
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ Text(it, style = MaterialTheme.typography.displaySmall)
+ }
+ }
+ }
+}
+
+@Composable
+@ReadOnlyComposable
+private fun Long.formatDateTimeValue(@StringRes id: Int): String {
+ if (this <= 0) return ""
+
+ return "${stringResource(id)}: $this"
+}
+
+@Preview
+@Composable
+private fun PreviewCard() {
+ DateTimeResultBlock(
+ modifier = Modifier,
+ dateDifference = DateDifference.Default(
+ months = 1,
+ days = 2,
+ hours = 3,
+ minutes = 4
+ )
+ )
+}
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeSelectorBlock.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeSelectorBlock.kt
new file mode 100644
index 00000000..55c913c0
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/components/DateTimeSelectorBlock.kt
@@ -0,0 +1,109 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference.components
+
+import android.text.format.DateFormat
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.squashable
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+@Composable
+internal fun DateTimeSelectorBlock(
+ modifier: Modifier,
+ title: String,
+ dateTime: LocalDateTime,
+ onClick: () -> Unit,
+ onTimeClick: () -> Unit,
+ onDateClick: () -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .squashable(
+ onClick = onClick,
+ interactionSource = remember { MutableInteractionSource() },
+ cornerRadiusRange = 8.dp..32.dp
+ )
+ .background(MaterialTheme.colorScheme.secondaryContainer)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(title, style = MaterialTheme.typography.labelMedium)
+
+ if (DateFormat.is24HourFormat(LocalContext.current)) {
+ Text(
+ modifier = Modifier.clickable(
+ indication = rememberRipple(),
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onTimeClick
+ ),
+ text = dateTime.format(time24Formatter),
+ style = MaterialTheme.typography.displaySmall,
+ maxLines = 1
+ )
+ } else {
+ Column(
+ modifier = Modifier.clickable(
+ indication = rememberRipple(),
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onTimeClick
+ )
+ ) {
+ Text(
+ text = dateTime.format(time12Formatter),
+ style = MaterialTheme.typography.displaySmall,
+ maxLines = 1
+ )
+ Text(
+ text = dateTime.format(mTimeFormatter),
+ style = MaterialTheme.typography.bodyLarge,
+ maxLines = 1
+ )
+ }
+ }
+
+ Text(
+ modifier = Modifier.clickable(
+ indication = rememberRipple(),
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onDateClick
+ ),
+ text = dateTime.format(dateFormatter),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+}
+
+private val time24Formatter by lazy { DateTimeFormatter.ofPattern("HH:mm") }
+private val time12Formatter by lazy { DateTimeFormatter.ofPattern("hh:mm") }
+private val dateFormatter by lazy { DateTimeFormatter.ofPattern("EEE, MMM d, y") }
+private val mTimeFormatter by lazy { DateTimeFormatter.ofPattern("a") }
diff --git a/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/navigation/DateDifferenceNavigation.kt b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/navigation/DateDifferenceNavigation.kt
new file mode 100644
index 00000000..d779d938
--- /dev/null
+++ b/feature/datedifference/src/main/java/com/sadellie/unitto/feature/datedifference/navigation/DateDifferenceNavigation.kt
@@ -0,0 +1,44 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference.navigation
+
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.navDeepLink
+import com.sadellie.unitto.core.base.TopLevelDestinations
+import com.sadellie.unitto.feature.datedifference.DateDifferenceRoute
+
+private val dateDifferenceRoute: String by lazy { TopLevelDestinations.DateDifference.route }
+
+fun NavGraphBuilder.dateDifferenceScreen(
+ navigateToMenu: () -> Unit,
+ navigateToSettings: () -> Unit
+) {
+ composable(
+ route = dateDifferenceRoute,
+ deepLinks = listOf(
+ navDeepLink { uriPattern = "app://com.sadellie.unitto/$dateDifferenceRoute" }
+ )
+ ) {
+ DateDifferenceRoute(
+ navigateToMenu = navigateToMenu,
+ navigateToSettings = navigateToSettings
+ )
+ }
+}
diff --git a/feature/datedifference/src/test/java/com/sadellie/unitto/feature/datedifference/DateDifferenceKtTest.kt b/feature/datedifference/src/test/java/com/sadellie/unitto/feature/datedifference/DateDifferenceKtTest.kt
new file mode 100644
index 00000000..7591a9ae
--- /dev/null
+++ b/feature/datedifference/src/test/java/com/sadellie/unitto/feature/datedifference/DateDifferenceKtTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.datedifference
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+class DateDifferenceKtTest {
+ private val fromatt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
+ private val `may 1 2023`: LocalDateTime = LocalDateTime.parse("2023-05-01 12:00", fromatt)
+ private val `may 2 2023`: LocalDateTime = LocalDateTime.parse("2023-05-02 12:00", fromatt)
+ private val `june 1 2023`: LocalDateTime = LocalDateTime.parse("2023-06-01 12:00", fromatt)
+
+ @Test
+ fun `same dates`() {
+ assertEquals(DateDifference.Zero, `may 1 2023` - `may 1 2023`)
+ }
+
+ @Test
+ fun `positive difference dates one day`() {
+ assertEquals(DateDifference.Default(days = 1), `may 1 2023` - `may 2 2023`)
+ }
+
+ @Test
+ fun `positive difference dates one minth`() {
+ assertEquals(DateDifference.Default(months = 1), `may 1 2023` - `june 1 2023`)
+ }
+
+ @Test
+ fun `negative difference dates one day`() {
+ assertEquals(DateDifference.Default(days = 1), `may 2 2023` - `may 1 2023`)
+ }
+}
diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt
index 0c1b0286..a8d17e30 100644
--- a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt
+++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.MenuButton
import com.sadellie.unitto.core.ui.common.PortraitLandscape
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index a8aa51e5..9badfbd5 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -31,8 +31,8 @@ dependencies {
implementation(libs.com.github.sadellie.themmo)
implementation(libs.org.burnoutcrew.composereorderable)
+ implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:model")))
- implementation(project(mapOf("path" to ":data:unitgroups")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":data:licenses")))
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt
index 7e9fd9e3..0bae6585 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/AboutScreen.kt
@@ -37,15 +37,17 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
import com.sadellie.unitto.core.ui.openLink
@@ -54,11 +56,11 @@ import com.sadellie.unitto.core.ui.openLink
internal fun AboutScreen(
navigateUpAction: () -> Unit,
navigateToThirdParty: () -> Unit,
- viewModel: SettingsViewModel
+ viewModel: SettingsViewModel = hiltViewModel()
) {
val mContext = LocalContext.current
val userPrefs = viewModel.userPrefs.collectAsStateWithLifecycle()
- var aboutItemClick: Int by rememberSaveable { mutableStateOf(0) }
+ var aboutItemClick: Int by rememberSaveable { mutableIntStateOf(0) }
var showDialog: Boolean by rememberSaveable { mutableStateOf(false) }
UnittoScreenWithLargeTopBar(
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
index ece6aa4b..e0f97a18 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsScreen.kt
@@ -19,7 +19,6 @@
package com.sadellie.unitto.feature.settings
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
@@ -42,14 +41,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig
-import com.sadellie.unitto.core.base.OUTPUT_FORMAT
-import com.sadellie.unitto.core.base.PRECISIONS
-import com.sadellie.unitto.core.base.SEPARATORS
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.base.TOP_LEVEL_DESTINATIONS
-import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.MenuButton
import com.sadellie.unitto.core.ui.common.UnittoListItem
@@ -58,12 +54,13 @@ import com.sadellie.unitto.core.ui.openLink
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.feature.settings.components.AlertDialogWithList
import com.sadellie.unitto.feature.settings.navigation.aboutRoute
+import com.sadellie.unitto.feature.settings.navigation.formattingRoute
import com.sadellie.unitto.feature.settings.navigation.themesRoute
import com.sadellie.unitto.feature.settings.navigation.unitsGroupRoute
@Composable
internal fun SettingsScreen(
- viewModel: SettingsViewModel,
+ viewModel: SettingsViewModel = hiltViewModel(),
menuButtonClick: () -> Unit,
navControllerAction: (String) -> Unit
) {
@@ -109,43 +106,18 @@ internal fun SettingsScreen(
)
}
- // GENERAL GROUP
- item { Header(stringResource(R.string.formatting_settings_group)) }
-
- // PRECISION
+ // FORMATTING
item {
ListItem(
leadingContent = {
Icon(
Icons.Default._123,
- stringResource(R.string.precision_setting),
+ stringResource(R.string.formatting_setting),
)
},
- headlineContent = { Text(stringResource(R.string.precision_setting)) },
- supportingContent = { Text(stringResource(R.string.precision_setting_support)) },
- modifier = Modifier.clickable { dialogState = DialogState.PRECISION }
- )
- }
-
- // SEPARATOR
- item {
- ListItem(
- headlineContent = { Text(stringResource(R.string.separator_setting)) },
- supportingContent = { Text(stringResource(R.string.separator_setting_support)) },
- modifier = Modifier
- .clickable { dialogState = DialogState.SEPARATOR }
- .padding(start = 40.dp)
- )
- }
-
- // OUTPUT FORMAT
- item {
- ListItem(
- headlineContent = { Text(stringResource(R.string.output_format_setting)) },
- supportingContent = { Text(stringResource(R.string.output_format_setting_support)) },
- modifier = Modifier
- .clickable { dialogState = DialogState.OUTPUT_FORMAT }
- .padding(start = 40.dp)
+ headlineContent = { Text(stringResource(R.string.formatting_setting)) },
+ supportingContent = { Text(stringResource(R.string.formatting_setting_support)) },
+ modifier = Modifier.clickable { navControllerAction(formattingRoute) }
)
}
@@ -259,35 +231,6 @@ internal fun SettingsScreen(
// Showing dialog
when (dialogState) {
- DialogState.PRECISION -> {
- AlertDialogWithList(
- title = stringResource(R.string.precision_setting),
- listItems = PRECISIONS,
- selectedItemIndex = userPrefs.value.digitsPrecision,
- selectAction = viewModel::updatePrecision,
- dismissAction = { resetDialog() },
- supportText = stringResource(R.string.precision_setting_info)
- )
- }
- DialogState.SEPARATOR -> {
- AlertDialogWithList(
- title = stringResource(R.string.separator_setting),
- listItems = SEPARATORS,
- selectedItemIndex = userPrefs.value.separator,
- selectAction = viewModel::updateSeparator,
- dismissAction = { resetDialog() }
- )
- }
- DialogState.OUTPUT_FORMAT -> {
- AlertDialogWithList(
- title = stringResource(R.string.output_format_setting),
- listItems = OUTPUT_FORMAT,
- selectedItemIndex = userPrefs.value.outputFormat,
- selectAction = viewModel::updateOutputFormat,
- dismissAction = { resetDialog() },
- supportText = stringResource(R.string.output_format_setting_info)
- )
- }
DialogState.START_SCREEN -> {
AlertDialogWithList(
title = stringResource(R.string.starting_screen_setting),
@@ -320,5 +263,5 @@ internal fun SettingsScreen(
* All possible states for alert dialog that opens when user clicks on settings.
*/
private enum class DialogState {
- NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, START_SCREEN, UNIT_LIST_SORTING
+ NONE, START_SCREEN, UNIT_LIST_SORTING
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
index 0b6a0598..d2f891c6 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/SettingsViewModel.kt
@@ -18,110 +18,27 @@
package com.sadellie.unitto.feature.settings
-import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.sadellie.unitto.core.ui.Formatter
-import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
-import com.sadellie.unitto.data.unitgroups.UnitGroupsRepository
import com.sadellie.unitto.data.userprefs.UserPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
-import io.github.sadellie.themmo.MonetMode
-import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
- private val unitGroupsRepository: UnitGroupsRepository,
) : ViewModel() {
- val userPrefs = userPrefsRepository.userPreferencesFlow
- .onEach { Formatter.setSeparator(it.separator) }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000),
+ val userPrefs = userPrefsRepository.allPreferencesFlow
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(5000),
UserPreferences()
)
- val shownUnitGroups = unitGroupsRepository.shownUnitGroups
- val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
-
- /**
- * @see UserPreferencesRepository.updateThemingMode
- */
- fun updateThemingMode(themingMode: ThemingMode) {
- viewModelScope.launch {
- userPrefsRepository.updateThemingMode(themingMode)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateDynamicTheme
- */
- fun updateDynamicTheme(enabled: Boolean) {
- viewModelScope.launch {
- userPrefsRepository.updateDynamicTheme(enabled)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateAmoledTheme
- */
- fun updateAmoledTheme(enabled: Boolean) {
- viewModelScope.launch {
- userPrefsRepository.updateAmoledTheme(enabled)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateCustomColor
- */
- fun updateCustomColor(color: Color) {
- viewModelScope.launch {
- userPrefsRepository.updateCustomColor(color)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateMonetMode
- */
- fun updateMonetMode(monetMode: MonetMode) {
- viewModelScope.launch {
- userPrefsRepository.updateMonetMode(monetMode)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateDigitsPrecision
- */
- fun updatePrecision(precision: Int) {
- viewModelScope.launch {
- userPrefsRepository.updateDigitsPrecision(precision)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateSeparator
- */
- fun updateSeparator(separator: Int) {
- viewModelScope.launch {
- userPrefsRepository.updateSeparator(separator)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateOutputFormat
- */
- fun updateOutputFormat(outputFormat: Int) {
- viewModelScope.launch {
- userPrefsRepository.updateOutputFormat(outputFormat)
- }
- }
/**
* @see UserPreferencesRepository.updateVibrations
@@ -141,46 +58,6 @@ class SettingsViewModel @Inject constructor(
}
}
- /**
- * @see UnitGroupsRepository.markUnitGroupAsHidden
- * @see UserPreferencesRepository.updateShownUnitGroups
- */
- fun hideUnitGroup(unitGroup: UnitGroup) {
- viewModelScope.launch {
- unitGroupsRepository.markUnitGroupAsHidden(unitGroup)
- userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
- }
- }
-
- /**
- * @see UnitGroupsRepository.markUnitGroupAsShown
- * @see UserPreferencesRepository.updateShownUnitGroups
- */
- fun returnUnitGroup(unitGroup: UnitGroup) {
- viewModelScope.launch {
- unitGroupsRepository.markUnitGroupAsShown(unitGroup)
- userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
- }
- }
-
- /**
- * @see UnitGroupsRepository.moveShownUnitGroups
- */
- fun onMove(from: ItemPosition, to: ItemPosition) {
- viewModelScope.launch {
- unitGroupsRepository.moveShownUnitGroups(from, to)
- }
- }
-
- /**
- * @see UserPreferencesRepository.updateShownUnitGroups
- */
- fun onDragEnd() {
- viewModelScope.launch {
- userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
- }
- }
-
/**
* @see UserPreferencesRepository.updateToolsExperiment
*/
@@ -207,20 +84,4 @@ class SettingsViewModel @Inject constructor(
userPrefsRepository.updateUnitConverterSorting(sorting)
}
}
-
- /**
- * Prevent from dragging over non-draggable items (headers and hidden)
- *
- * @param pos Position we are dragging over.
- * @return True if can drag over given item.
- */
- fun canDragOver(pos: ItemPosition) = shownUnitGroups.value.any { it == pos.key }
-
- init {
- viewModelScope.launch {
- unitGroupsRepository.updateShownGroups(
- userPrefsRepository.userPreferencesFlow.first().shownUnitGroups
- )
- }
- }
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThirdPartyLicensesScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThirdPartyLicensesScreen.kt
index bb00bc3c..2bba1fe2 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThirdPartyLicensesScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThirdPartyLicensesScreen.kt
@@ -36,7 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.R
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
import com.sadellie.unitto.core.ui.openLink
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/components/AlertDialogWithList.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/components/AlertDialogWithList.kt
index 179d669f..6eda5080 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/components/AlertDialogWithList.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/components/AlertDialogWithList.kt
@@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.feature.settings.R
+import com.sadellie.unitto.core.base.R
/**
* Alert dialog that has a list of options in it
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingScreen.kt
new file mode 100644
index 00000000..a9929b51
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingScreen.kt
@@ -0,0 +1,275 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.formatting
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Architecture
+import androidx.compose.material.icons.filled.EMobiledata
+import androidx.compose.material.icons.filled._123
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.base.MAX_PRECISION
+import com.sadellie.unitto.core.base.OutputFormat
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.base.Separator
+import com.sadellie.unitto.core.ui.common.NavigateUpButton
+import com.sadellie.unitto.core.ui.common.SegmentedButton
+import com.sadellie.unitto.core.ui.common.SegmentedButtonsRow
+import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
+import com.sadellie.unitto.core.ui.common.UnittoSlider
+import com.sadellie.unitto.core.ui.common.squashable
+import com.sadellie.unitto.core.ui.common.textfield.formatExpression
+import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
+import kotlin.math.roundToInt
+
+@Composable
+fun FormattingRoute(
+ viewModel: FormattingViewModel = hiltViewModel(),
+ navigateUpAction: () -> Unit,
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle()
+
+ FormattingScreen(
+ navigateUpAction = navigateUpAction,
+ uiState = uiState.value,
+ onPrecisionChange = viewModel::updatePrecision,
+ onSeparatorChange = viewModel::updateSeparator,
+ onOutputFormatChange = viewModel::updateOutputFormat,
+ togglePreview = viewModel::togglePreview
+ )
+}
+
+@Composable
+fun FormattingScreen(
+ navigateUpAction: () -> Unit,
+ uiState: FormattingUIState,
+ onPrecisionChange: (Int) -> Unit,
+ onSeparatorChange: (Int) -> Unit,
+ onOutputFormatChange: (Int) -> Unit,
+ togglePreview: () -> Unit,
+ precisions: ClosedFloatingPointRange = 0f..16f, // 16th is a MAX_PRECISION (1000)
+) {
+ val resources = LocalContext.current.resources
+
+ val precisionText: String by remember(uiState.precision, uiState.formatterSymbols) {
+ derivedStateOf {
+ return@derivedStateOf if (uiState.precision >= precisions.endInclusive) {
+ resources.getString(
+ R.string.max_precision,
+ MAX_PRECISION.toString().formatExpression(uiState.formatterSymbols)
+ )
+ } else {
+ uiState.precision.toString()
+ }
+ }
+ }
+
+ UnittoScreenWithLargeTopBar(
+ title = stringResource(R.string.formatting_setting),
+ navigationIcon = { NavigateUpButton(navigateUpAction) },
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(paddingValues)
+ ) {
+ item("preview") {
+ Column(
+ Modifier
+ .padding(16.dp)
+ .squashable(
+ onClick = togglePreview,
+ cornerRadiusRange = 8.dp..32.dp,
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ .background(MaterialTheme.colorScheme.secondaryContainer)
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.formatting_setting_preview_box_label),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Text(
+ text = uiState.preview,
+ style = NumbersTextStyleDisplayMedium,
+ maxLines = 1,
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ textAlign = TextAlign.End,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
+
+ item("precision_label") {
+ ListItem(
+ leadingContent = {
+ Icon(Icons.Default.Architecture, stringResource(R.string.precision_setting))
+ },
+ headlineContent = {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.precision_setting))
+ Text(precisionText)
+ }
+ },
+ supportingContent = {
+ Text(stringResource(R.string.precision_setting_support))
+ }
+ )
+ }
+
+ item("precision_slider") {
+ UnittoSlider(
+ modifier = Modifier.padding(start = 56.dp, end = 16.dp),
+ value = uiState.precision.toFloat(),
+ valueRange = precisions,
+ onValueChange = { onPrecisionChange(it.roundToInt()) },
+ )
+ }
+
+ item("separator_label") {
+ ListItem(
+ leadingContent = {
+ Icon(Icons.Default._123, stringResource(R.string.precision_setting))
+ },
+ headlineContent = { Text(stringResource(R.string.separator_setting)) },
+ supportingContent = { Text(stringResource(R.string.separator_setting_support)) },
+ )
+ }
+
+ item("separator") {
+ Row(
+ Modifier
+ .horizontalScroll(rememberScrollState())
+ .wrapContentWidth()
+ .padding(start = 56.dp)
+ ) {
+ SegmentedButtonsRow {
+ SegmentedButton(
+ label = stringResource(R.string.space),
+ onClick = { onSeparatorChange(Separator.SPACE) },
+ selected = Separator.SPACE == uiState.separator
+ )
+ SegmentedButton(
+ label = stringResource(R.string.period),
+ onClick = { onSeparatorChange(Separator.PERIOD) },
+ selected = Separator.PERIOD == uiState.separator
+ )
+ SegmentedButton(
+ label = stringResource(R.string.comma),
+ onClick = { onSeparatorChange(Separator.COMMA) },
+ selected = Separator.COMMA == uiState.separator
+ )
+ }
+ }
+ }
+
+ item("output_format_label") {
+ ListItem(
+ leadingContent = {
+ Icon(Icons.Default.EMobiledata, stringResource(R.string.precision_setting))
+ },
+ headlineContent = { Text(stringResource(R.string.exponential_notation_setting)) },
+ supportingContent = { Text(stringResource(R.string.exponential_notation_setting_support)) }
+ )
+ }
+
+ item("output_format") {
+ Row(
+ Modifier
+ .horizontalScroll(rememberScrollState())
+ .wrapContentWidth()
+ .padding(start = 56.dp)
+ ) {
+ SegmentedButtonsRow {
+ SegmentedButton(
+ label = stringResource(R.string.auto_label),
+ onClick = { onOutputFormatChange(OutputFormat.ALLOW_ENGINEERING) },
+ selected = OutputFormat.ALLOW_ENGINEERING == uiState.outputFormat
+ )
+ SegmentedButton(
+ label = stringResource(R.string.enabled_label),
+ onClick = { onOutputFormatChange(OutputFormat.FORCE_ENGINEERING) },
+ selected = OutputFormat.FORCE_ENGINEERING == uiState.outputFormat
+ )
+ SegmentedButton(
+ label = stringResource(R.string.disabled_label),
+ onClick = { onOutputFormatChange(OutputFormat.PLAIN) },
+ selected = OutputFormat.PLAIN == uiState.outputFormat
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewFormattingScreen() {
+ var currentPrecision by remember { mutableIntStateOf(6) }
+ var currentSeparator by remember { mutableIntStateOf(Separator.COMMA) }
+ var currentOutputFormat by remember { mutableIntStateOf(OutputFormat.PLAIN) }
+
+ FormattingScreen(
+ uiState = FormattingUIState(
+ preview = "123456.789",
+ precision = 16,
+ separator = Separator.SPACE,
+ outputFormat = OutputFormat.PLAIN
+ ),
+ onPrecisionChange = { currentPrecision = it },
+ onSeparatorChange = { currentSeparator = it },
+ onOutputFormatChange = { currentOutputFormat = it },
+ navigateUpAction = {},
+ togglePreview = {}
+ )
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingUIState.kt
new file mode 100644
index 00000000..fb23227d
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingUIState.kt
@@ -0,0 +1,29 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.formatting
+
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+
+data class FormattingUIState(
+ val preview: String = "",
+ val precision: Int = 0,
+ val separator: Int? = null,
+ val outputFormat: Int? = null,
+ val formatterSymbols: FormatterSymbols = FormatterSymbols.Spaces
+)
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingViewModel.kt
new file mode 100644
index 00000000..a72de138
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/formatting/FormattingViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.formatting
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.core.base.MAX_PRECISION
+import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.formatExpression
+import com.sadellie.unitto.data.common.setMinimumRequiredScale
+import com.sadellie.unitto.data.common.toStringWith
+import com.sadellie.unitto.data.common.trimZeros
+import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.math.BigDecimal
+import javax.inject.Inject
+import kotlin.math.ceil
+
+@HiltViewModel
+class FormattingViewModel @Inject constructor(
+ private val userPreferencesRepository: UserPreferencesRepository
+) : ViewModel() {
+ private val _mainPreferences = userPreferencesRepository.mainPreferencesFlow
+ private val _fractional = MutableStateFlow(false)
+
+ val uiState = combine(_mainPreferences, _fractional) { mainPrefs, fractional ->
+ val formatterSymbols = AllFormatterSymbols.getById(mainPrefs.separator)
+
+ return@combine FormattingUIState(
+ preview = updatePreview(
+ fractional = fractional,
+ precision = mainPrefs.digitsPrecision,
+ outputFormat = mainPrefs.outputFormat,
+ formatterSymbols = formatterSymbols
+ ),
+ precision = mainPrefs.digitsPrecision,
+ separator = mainPrefs.separator,
+ outputFormat = mainPrefs.outputFormat,
+ formatterSymbols = formatterSymbols
+ )
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), FormattingUIState())
+
+ fun togglePreview() = _fractional.update { !it }
+
+ private fun updatePreview(
+ fractional: Boolean,
+ precision: Int,
+ outputFormat: Int,
+ formatterSymbols: FormatterSymbols
+ ): String {
+ val bigD = when {
+ fractional -> "0.${"1".padStart(precision, '0')}"
+ precision > 0 -> "123456.${"789123456".repeat(ceil(precision.toDouble() / 9.0).toInt())}"
+ else -> "123456"
+ }
+
+ return BigDecimal(bigD)
+ .setMinimumRequiredScale(precision)
+ .trimZeros()
+ .toStringWith(outputFormat)
+ .formatExpression(formatterSymbols)
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateDigitsPrecision
+ */
+ fun updatePrecision(precision: Int) {
+ viewModelScope.launch {
+ // In UI the slider for precision goes from 0 to 16, where 16 is treated as 1000 (MAX)
+ val newPrecision = if (precision > 15) MAX_PRECISION else precision
+ userPreferencesRepository.updateDigitsPrecision(newPrecision)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateSeparator
+ */
+ fun updateSeparator(separator: Int) {
+ viewModelScope.launch {
+ userPreferencesRepository.updateSeparator(separator)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateOutputFormat
+ */
+ fun updateOutputFormat(outputFormat: Int) {
+ viewModelScope.launch {
+ userPreferencesRepository.updateOutputFormat(outputFormat)
+ }
+ }
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
index cc2a874e..96ca4201 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
@@ -21,16 +21,15 @@ package com.sadellie.unitto.feature.settings.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
-import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.feature.settings.AboutScreen
import com.sadellie.unitto.feature.settings.SettingsScreen
-import com.sadellie.unitto.feature.settings.SettingsViewModel
-import com.sadellie.unitto.feature.settings.ThemesRoute
import com.sadellie.unitto.feature.settings.ThirdPartyLicensesScreen
-import com.sadellie.unitto.feature.settings.UnitGroupsScreen
+import com.sadellie.unitto.feature.settings.formatting.FormattingRoute
+import com.sadellie.unitto.feature.settings.themes.ThemesRoute
+import com.sadellie.unitto.feature.settings.unitgroups.UnitGroupsScreen
import io.github.sadellie.themmo.ThemmoController
private val settingsGraph: String by lazy { TopLevelDestinations.Settings.route }
@@ -39,9 +38,10 @@ internal const val themesRoute = "themes_route"
internal const val unitsGroupRoute = "units_group_route"
internal const val thirdPartyRoute = "third_party_route"
internal const val aboutRoute = "about_route"
+internal const val formattingRoute = "formatting_route"
-fun NavController.navigateToSettings(builder: NavOptionsBuilder.() -> Unit) {
- navigate(settingsRoute, builder)
+fun NavController.navigateToSettings() {
+ navigate(settingsRoute)
}
fun NavController.navigateToUnitGroups() {
@@ -49,7 +49,6 @@ fun NavController.navigateToUnitGroups() {
}
fun NavGraphBuilder.settingGraph(
- settingsViewModel: SettingsViewModel,
themmoController: ThemmoController,
navController: NavHostController,
menuButtonClick: () -> Unit
@@ -57,37 +56,40 @@ fun NavGraphBuilder.settingGraph(
navigation(settingsRoute, settingsGraph) {
composable(settingsRoute) {
SettingsScreen(
- viewModel = settingsViewModel,
- menuButtonClick = menuButtonClick
- ) { route -> navController.navigate(route) }
+ menuButtonClick = menuButtonClick,
+ navControllerAction = navController::navigate
+ )
}
composable(themesRoute) {
ThemesRoute(
- navigateUpAction = { navController.navigateUp() },
+ navigateUpAction = navController::navigateUp,
themmoController = themmoController,
- viewModel = settingsViewModel
)
}
composable(thirdPartyRoute) {
ThirdPartyLicensesScreen(
- navigateUpAction = { navController.navigateUp() }
+ navigateUpAction = navController::navigateUp,
)
}
composable(aboutRoute) {
AboutScreen(
- navigateUpAction = { navController.navigateUp() },
+ navigateUpAction = navController::navigateUp,
navigateToThirdParty = { navController.navigate(thirdPartyRoute) },
- viewModel = settingsViewModel
)
}
composable(unitsGroupRoute) {
UnitGroupsScreen(
- viewModel = settingsViewModel,
- navigateUpAction = { navController.navigateUp() }
+ navigateUpAction = navController::navigateUp,
+ )
+ }
+
+ composable(formattingRoute) {
+ FormattingRoute(
+ navigateUpAction = navController::navigateUp
)
}
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt
similarity index 89%
rename from feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt
rename to feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt
index 720b4393..22cb2c82 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/ThemesScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesScreen.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package com.sadellie.unitto.feature.settings
+package com.sadellie.unitto.feature.settings.themes
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
@@ -41,15 +41,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.core.ui.R
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.Header
import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.SegmentedButton
@@ -82,7 +81,7 @@ private val colorSchemes: List by lazy {
internal fun ThemesRoute(
navigateUpAction: () -> Unit = {},
themmoController: ThemmoController,
- viewModel: SettingsViewModel
+ viewModel: ThemesViewModel = hiltViewModel()
) {
ThemesScreen(
navigateUpAction = navigateUpAction,
@@ -133,16 +132,6 @@ private fun ThemesScreen(
monetMode: MonetMode,
onMonetModeChange: (MonetMode) -> Unit
) {
- val themingModes by remember {
- mutableStateOf(
- mapOf(
- ThemingMode.AUTO to (R.string.force_auto_mode to Icons.Outlined.HdrAuto),
- ThemingMode.FORCE_LIGHT to (R.string.force_light_mode to Icons.Outlined.LightMode),
- ThemingMode.FORCE_DARK to (R.string.force_dark_mode to Icons.Outlined.DarkMode)
- )
- )
- }
-
UnittoScreenWithLargeTopBar(
title = stringResource(R.string.theme_setting),
navigationIcon = { NavigateUpButton(navigateUpAction) }
@@ -169,15 +158,24 @@ private fun ThemesScreen(
.wrapContentWidth()
) {
SegmentedButtonsRow(modifier = Modifier.padding(56.dp, 8.dp, 24.dp, 2.dp)) {
- themingModes.forEach { (mode, visuals) ->
- val (label, icon) = visuals
- SegmentedButton(
- label = stringResource(label),
- onClick = { onThemeChange(mode) },
- selected = mode == currentThemingMode,
- icon = icon
- )
- }
+ SegmentedButton(
+ label = stringResource(R.string.auto_label),
+ onClick = { onThemeChange(ThemingMode.AUTO) },
+ selected = ThemingMode.AUTO == currentThemingMode,
+ icon = Icons.Outlined.HdrAuto
+ )
+ SegmentedButton(
+ label = stringResource(R.string.force_light_mode),
+ onClick = { onThemeChange(ThemingMode.FORCE_LIGHT) },
+ selected = ThemingMode.FORCE_LIGHT == currentThemingMode,
+ icon = Icons.Outlined.LightMode
+ )
+ SegmentedButton(
+ label = stringResource(R.string.force_dark_mode),
+ onClick = { onThemeChange(ThemingMode.FORCE_DARK) },
+ selected = ThemingMode.FORCE_DARK == currentThemingMode,
+ icon = Icons.Outlined.DarkMode
+ )
}
}
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt
new file mode 100644
index 00000000..d28bb490
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/themes/ThemesViewModel.kt
@@ -0,0 +1,80 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.themes
+
+import androidx.compose.ui.graphics.Color
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import io.github.sadellie.themmo.MonetMode
+import io.github.sadellie.themmo.ThemingMode
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class ThemesViewModel @Inject constructor(
+ private val userPrefsRepository: UserPreferencesRepository
+) : ViewModel() {
+
+ /**
+ * @see UserPreferencesRepository.updateThemingMode
+ */
+ fun updateThemingMode(themingMode: ThemingMode) {
+ viewModelScope.launch {
+ userPrefsRepository.updateThemingMode(themingMode)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateDynamicTheme
+ */
+ fun updateDynamicTheme(enabled: Boolean) {
+ viewModelScope.launch {
+ userPrefsRepository.updateDynamicTheme(enabled)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateAmoledTheme
+ */
+ fun updateAmoledTheme(enabled: Boolean) {
+ viewModelScope.launch {
+ userPrefsRepository.updateAmoledTheme(enabled)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateCustomColor
+ */
+ fun updateCustomColor(color: Color) {
+ viewModelScope.launch {
+ userPrefsRepository.updateCustomColor(color)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateMonetMode
+ */
+ fun updateMonetMode(monetMode: MonetMode) {
+ viewModelScope.launch {
+ userPrefsRepository.updateMonetMode(monetMode)
+ }
+ }
+}
diff --git a/data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsRepository .kt
similarity index 97%
rename from data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt
rename to feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsRepository .kt
index 44fbe968..eb0decf0 100644
--- a/data/unitgroups/src/main/java/com/sadellie/unitto/data/unitgroups/UnitGroupsRepository.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsRepository .kt
@@ -1,6 +1,6 @@
/*
* Unitto is a unit converter for Android
- * Copyright (c) 2022-2023 Elshan Agaev
+ * Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package com.sadellie.unitto.data.unitgroups
+package com.sadellie.unitto.feature.settings.unitgroups
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt
similarity index 97%
rename from feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt
rename to feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt
index ed93cd2e..6956c7ed 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/UnitGroupsScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsScreen.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package com.sadellie.unitto.feature.settings
+package com.sadellie.unitto.feature.settings.unitgroups
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
@@ -47,10 +47,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.Header
-import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
-import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.ui.common.NavigateUpButton
+import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
@@ -59,7 +60,7 @@ import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun UnitGroupsScreen(
- viewModel: SettingsViewModel,
+ viewModel: UnitGroupsViewModel = hiltViewModel(),
navigateUpAction: () -> Unit
) {
UnittoScreenWithLargeTopBar(
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt
new file mode 100644
index 00000000..229f6119
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/unitgroups/UnitGroupsViewModel.kt
@@ -0,0 +1,94 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.unitgroups
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.data.model.UnitGroup
+import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import org.burnoutcrew.reorderable.ItemPosition
+import javax.inject.Inject
+
+@HiltViewModel
+class UnitGroupsViewModel @Inject constructor(
+ private val userPrefsRepository: UserPreferencesRepository,
+ private val unitGroupsRepository: UnitGroupsRepository,
+) : ViewModel() {
+ val shownUnitGroups = unitGroupsRepository.shownUnitGroups
+ val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
+
+ /**
+ * @see UnitGroupsRepository.markUnitGroupAsHidden
+ * @see UserPreferencesRepository.updateShownUnitGroups
+ */
+ fun hideUnitGroup(unitGroup: UnitGroup) {
+ viewModelScope.launch {
+ unitGroupsRepository.markUnitGroupAsHidden(unitGroup)
+ userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
+ }
+ }
+
+ /**
+ * @see UnitGroupsRepository.markUnitGroupAsShown
+ * @see UserPreferencesRepository.updateShownUnitGroups
+ */
+ fun returnUnitGroup(unitGroup: UnitGroup) {
+ viewModelScope.launch {
+ unitGroupsRepository.markUnitGroupAsShown(unitGroup)
+ userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
+ }
+ }
+
+ /**
+ * @see UnitGroupsRepository.moveShownUnitGroups
+ */
+ fun onMove(from: ItemPosition, to: ItemPosition) {
+ viewModelScope.launch {
+ unitGroupsRepository.moveShownUnitGroups(from, to)
+ }
+ }
+
+ /**
+ * @see UserPreferencesRepository.updateShownUnitGroups
+ */
+ fun onDragEnd() {
+ viewModelScope.launch {
+ userPrefsRepository.updateShownUnitGroups(unitGroupsRepository.shownUnitGroups.value)
+ }
+ }
+
+ /**
+ * Prevent from dragging over non-draggable items (headers and hidden)
+ *
+ * @param pos Position we are dragging over.
+ * @return True if can drag over given item.
+ */
+ fun canDragOver(pos: ItemPosition) = shownUnitGroups.value.any { it == pos.key }
+
+ init {
+ viewModelScope.launch {
+ unitGroupsRepository.updateShownGroups(
+ userPrefsRepository.mainPreferencesFlow.first().shownUnitGroups
+ )
+ }
+ }
+}
diff --git a/feature/unitslist/build.gradle.kts b/feature/unitslist/build.gradle.kts
index 6df670f4..065310e6 100644
--- a/feature/unitslist/build.gradle.kts
+++ b/feature/unitslist/build.gradle.kts
@@ -28,11 +28,8 @@ android {
}
dependencies {
- implementation(libs.com.github.sadellie.themmo)
-
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":data:units")))
implementation(project(mapOf("path" to ":data:database")))
- implementation(project(mapOf("path" to ":data:unitgroups")))
}
\ No newline at end of file
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/LeftSideScreen.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/LeftSideScreen.kt
index 19d47686..44531ac2 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/LeftSideScreen.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/LeftSideScreen.kt
@@ -45,6 +45,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.feature.unitslist.components.ChipsRow
import com.sadellie.unitto.feature.unitslist.components.SearchBar
@@ -93,9 +94,9 @@ internal fun LeftSideScreen(
SearchBar(
title = stringResource(R.string.units_screen_from),
value = uiState.value.searchQuery,
- onValueChange = { viewModel.onSearchQueryChange(it) },
+ onValueChange = { viewModel.onSearchQueryChange(it, false) },
favoritesOnly = uiState.value.favoritesOnly,
- favoriteAction = { viewModel.toggleFavoritesOnly() },
+ favoriteAction = { viewModel.toggleFavoritesOnly(false) },
navigateUpAction = navigateUp,
focusManager = focusManager,
scrollBehavior = scrollBehavior
@@ -103,7 +104,7 @@ internal fun LeftSideScreen(
ChipsRow(
chosenUnitGroup = uiState.value.chosenUnitGroup,
items = uiState.value.shownUnitGroups,
- selectAction = { viewModel.toggleSelectedChip(it) },
+ selectAction = { viewModel.toggleSelectedChip(it, false) },
lazyListState = chipsRowLazyListState,
navigateToSettingsAction = navigateToSettingsAction
)
@@ -129,7 +130,7 @@ internal fun LeftSideScreen(
isSelected = currentUnitId == unit.unitId,
selectAction = {
selectAction(it)
- viewModel.onSearchQueryChange("")
+ viewModel.onSearchQueryChange("", false)
focusManager.clearFocus(true)
navigateUp()
},
@@ -146,7 +147,7 @@ internal fun LeftSideScreen(
if (currentUnitId == null) return@LaunchedEffect
// This is still wrong, but works good enough.
// Ideally we shouldn't use uiState.value.shownUnitGroups
- viewModel.setSelectedChip(currentUnitId)
+ viewModel.setSelectedChip(currentUnitId, false)
val groupToSelect = uiState.value.shownUnitGroups.indexOf(uiState.value.chosenUnitGroup)
if (groupToSelect > -1) {
chipsRowLazyListState.animateScrollToItem(groupToSelect)
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/RightSideScreen.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/RightSideScreen.kt
index 21f94644..d334d101 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/RightSideScreen.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/RightSideScreen.kt
@@ -32,7 +32,9 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.sadellie.unitto.core.ui.Formatter
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.formatExpression
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.NumberBaseUnit
import com.sadellie.unitto.data.model.UnitGroup
@@ -60,7 +62,7 @@ internal fun RightSideScreen(
navigateUp: () -> Unit,
navigateToSettingsAction: () -> Unit,
selectAction: (AbstractUnit) -> Unit,
- inputValue: String,
+ inputValue: String?,
unitFrom: AbstractUnit
) {
val uiState = viewModel.mainFlow.collectAsStateWithLifecycle()
@@ -69,12 +71,21 @@ internal fun RightSideScreen(
val convertMethod: (AbstractUnit) -> String = try {
val inputAsBigDecimal = BigDecimal(inputValue)
- if (unitFrom.group == UnitGroup.NUMBER_BASE) {
- { (convertForSecondaryNumberBase(inputValue, unitFrom, it)) }
- } else {
- { convertForSecondary(inputAsBigDecimal, unitFrom, it) }
+
+ when {
+ inputValue.isNullOrEmpty() -> { { "" } }
+
+ unitFrom.group == UnitGroup.NUMBER_BASE -> {
+ { (convertForSecondaryNumberBase(inputValue, unitFrom, it)) }
+ }
+
+ else -> {
+ {
+ convertForSecondary(inputAsBigDecimal, unitFrom, it, uiState.value.formatterSymbols)
+ }
+ }
}
- } catch(e: Exception) {
+ } catch (e: Exception) {
{ "" }
}
@@ -85,11 +96,11 @@ internal fun RightSideScreen(
title = stringResource(R.string.units_screen_to),
value = uiState.value.searchQuery,
onValueChange = {
- viewModel.onSearchQueryChange(it, false)
+ viewModel.onSearchQueryChange(it, true)
},
favoritesOnly = uiState.value.favoritesOnly,
favoriteAction = {
- viewModel.toggleFavoritesOnly(false)
+ viewModel.toggleFavoritesOnly(true)
},
navigateUpAction = navigateUp,
focusManager = focusManager,
@@ -116,7 +127,7 @@ internal fun RightSideScreen(
isSelected = currentUnit == unit.unitId,
selectAction = {
selectAction(it)
- viewModel.onSearchQueryChange("")
+ viewModel.onSearchQueryChange("", true)
focusManager.clearFocus(true)
navigateUp()
},
@@ -131,15 +142,26 @@ internal fun RightSideScreen(
}
}
-private fun convertForSecondary(inputValue: BigDecimal, unitFrom: AbstractUnit, unitTo: AbstractUnit): String {
- return Formatter.format(
- unitFrom.convert(unitTo, inputValue, 3).toPlainString()
- ) + " "
+private fun convertForSecondary(
+ inputValue: BigDecimal,
+ unitFrom: AbstractUnit,
+ unitTo: AbstractUnit,
+ formatterSymbols: FormatterSymbols
+): String {
+ return unitFrom.convert(unitTo, inputValue, 3).toPlainString()
+ .formatExpression(formatterSymbols) + " "
}
-private fun convertForSecondaryNumberBase(inputValue: String, unitFrom: AbstractUnit, unitTo: AbstractUnit): String {
+private fun convertForSecondaryNumberBase(
+ inputValue: String,
+ unitFrom: AbstractUnit,
+ unitTo: AbstractUnit
+): String {
return try {
- (unitFrom as NumberBaseUnit).convertToBase(inputValue, (unitTo as NumberBaseUnit).base) + " "
+ (unitFrom as NumberBaseUnit).convertToBase(
+ inputValue,
+ (unitTo as NumberBaseUnit).base
+ ) + " "
} catch (e: NumberFormatException) {
""
} catch (e: ClassCastException) {
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/SecondScreenUIState.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/SecondScreenUIState.kt
index f012c8ea..57e15201 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/SecondScreenUIState.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/SecondScreenUIState.kt
@@ -18,6 +18,7 @@
package com.sadellie.unitto.feature.unitslist
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.UnitGroup
@@ -35,5 +36,6 @@ data class SecondScreenUIState(
val unitsToShow: Map> = emptyMap(),
val searchQuery: String = "",
val shownUnitGroups: List = listOf(),
- val chosenUnitGroup: UnitGroup? = null
+ val chosenUnitGroup: UnitGroup? = null,
+ val formatterSymbols: FormatterSymbols = FormatterSymbols.Spaces,
)
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt
index af2fea90..01b5a594 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/UnitsListViewModel.kt
@@ -21,13 +21,13 @@ package com.sadellie.unitto.feature.unitslist
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
import com.sadellie.unitto.data.database.UnitsEntity
import com.sadellie.unitto.data.database.UnitsRepository
import com.sadellie.unitto.data.model.AbstractUnit
import com.sadellie.unitto.data.model.UnitGroup
-import com.sadellie.unitto.data.unitgroups.UnitGroupsRepository
import com.sadellie.unitto.data.units.AllUnitsRepository
-import com.sadellie.unitto.data.userprefs.UserPreferences
+import com.sadellie.unitto.data.userprefs.MainPreferences
import com.sadellie.unitto.data.userprefs.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -47,49 +47,47 @@ class UnitsListViewModel @Inject constructor(
private val allUnitsRepository: AllUnitsRepository,
private val mContext: Application,
private val userPrefsRepository: UserPreferencesRepository,
- unitGroupsRepository: UnitGroupsRepository,
) : ViewModel() {
- private val _userPrefs: StateFlow =
- userPrefsRepository.userPreferencesFlow.stateIn(
+ private val _userPrefs: StateFlow =
+ userPrefsRepository.mainPreferencesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
- UserPreferences()
+ MainPreferences()
)
private val _unitsToShow = MutableStateFlow(emptyMap>())
private val _searchQuery = MutableStateFlow("")
private val _chosenUnitGroup: MutableStateFlow = MutableStateFlow(null)
- private val _shownUnitGroups = unitGroupsRepository.shownUnitGroups
val mainFlow = combine(
_userPrefs,
_unitsToShow,
_searchQuery,
_chosenUnitGroup,
- _shownUnitGroups,
- ) { userPrefs, unitsToShow, searchQuery, chosenUnitGroup, shownUnitGroups ->
+ ) { userPrefs, unitsToShow, searchQuery, chosenUnitGroup ->
return@combine SecondScreenUIState(
favoritesOnly = userPrefs.unitConverterFavoritesOnly,
unitsToShow = unitsToShow,
searchQuery = searchQuery,
chosenUnitGroup = chosenUnitGroup,
- shownUnitGroups = shownUnitGroups,
+ shownUnitGroups = userPrefs.shownUnitGroups,
+ formatterSymbols = AllFormatterSymbols.getById(userPrefs.separator)
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SecondScreenUIState())
- fun toggleFavoritesOnly(hideBrokenCurrencies: Boolean = true) {
+ fun toggleFavoritesOnly(hideBrokenUnits: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateUnitConverterFavoritesOnly(
!_userPrefs.value.unitConverterFavoritesOnly
)
- loadUnitsToShow(hideBrokenCurrencies)
+ loadUnitsToShow(hideBrokenUnits)
}
}
- fun onSearchQueryChange(newValue: String, hideBrokenCurrencies: Boolean = true) {
+ fun onSearchQueryChange(newValue: String, hideBrokenUnits: Boolean) {
_searchQuery.update { newValue }
- loadUnitsToShow(hideBrokenCurrencies)
+ loadUnitsToShow(hideBrokenUnits)
}
/**
@@ -97,9 +95,9 @@ class UnitsListViewModel @Inject constructor(
*
* @param unit Will find group for unit with this id.
*/
- fun setSelectedChip(unit: String, hideBrokenCurrencies: Boolean = true) {
+ fun setSelectedChip(unit: String, hideBrokenUnits: Boolean) {
_chosenUnitGroup.update { allUnitsRepository.getById(unit).group }
- loadUnitsToShow(hideBrokenCurrencies)
+ loadUnitsToShow(hideBrokenUnits)
}
/**
@@ -110,31 +108,30 @@ class UnitsListViewModel @Inject constructor(
*
* @param unitGroup [UnitGroup], currently selected chip.
*/
- fun toggleSelectedChip(unitGroup: UnitGroup, hideBrokenCurrencies: Boolean = true) {
+ fun toggleSelectedChip(unitGroup: UnitGroup, hideBrokenUnits: Boolean) {
val newUnitGroup = if (_chosenUnitGroup.value == unitGroup) null else unitGroup
_chosenUnitGroup.update { newUnitGroup }
- loadUnitsToShow(hideBrokenCurrencies)
+ loadUnitsToShow(hideBrokenUnits)
}
/**
* Filters and groups [AllUnitsRepository.allUnits] in coroutine
*
- * @param hideBrokenCurrencies Decide whether or not we are on left side. Need it because right side requires
- * us to mark disabled currency units
+ * @param hideBrokenUnits Broken units come from currencies API (basic unit is zero)
*/
private fun loadUnitsToShow(
- hideBrokenCurrencies: Boolean
+ hideBrokenUnits: Boolean
) {
viewModelScope.launch {
// This is mostly not UI related stuff and viewModelScope.launch uses Dispatchers.Main
// So we switch to Default
withContext(Dispatchers.Default) {
val unitsToShow = allUnitsRepository.filterUnits(
- hideBrokenCurrencies = hideBrokenCurrencies,
+ hideBrokenUnits = hideBrokenUnits,
chosenUnitGroup = _chosenUnitGroup.value,
favoritesOnly = _userPrefs.value.unitConverterFavoritesOnly,
searchQuery = _searchQuery.value,
- allUnitsGroups = _shownUnitGroups.value,
+ allUnitsGroups = _userPrefs.value.shownUnitGroups,
sorting = _userPrefs.value.unitConverterSorting
)
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/ChipsRow.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/ChipsRow.kt
index 4a7e362b..2261fbb0 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/ChipsRow.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/ChipsRow.kt
@@ -46,9 +46,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.ALL_UNIT_GROUPS
import com.sadellie.unitto.data.model.UnitGroup
-import com.sadellie.unitto.feature.unitslist.R
/**
* Row of chips with [UnitGroup]s. Temporary solution
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchBar.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchBar.kt
index 7cee6021..6e78872c 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchBar.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchBar.kt
@@ -18,6 +18,7 @@
package com.sadellie.unitto.feature.unitslist.components
+import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
@@ -27,7 +28,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
-import androidx.compose.animation.with
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
@@ -61,8 +62,8 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.NavigateUpButton
-import com.sadellie.unitto.feature.unitslist.R
/**
* Search bar on the Second screen. Controls what will be shown in the list above this component
@@ -214,6 +215,7 @@ private fun SearchButton(
}
}
+@SuppressLint("UnusedContentLambdaTargetStateParameter")
@Composable
private fun FavoritesButton(
favoritesOnly: Boolean,
@@ -223,7 +225,7 @@ private fun FavoritesButton(
AnimatedContent(
targetState = favoritesOnly,
transitionSpec = {
- (scaleIn() with scaleOut()).using(SizeTransform(clip = false))
+ (scaleIn() togetherWith scaleOut()).using(SizeTransform(clip = false))
}
) {
Icon(
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchPlaceholder.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchPlaceholder.kt
index 0f8abbba..c0c72c38 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchPlaceholder.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/SearchPlaceholder.kt
@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.sadellie.unitto.feature.unitslist.R
+import com.sadellie.unitto.core.base.R
/**
* Placeholder that can be seen when there are no units found
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/UnitListItem.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/UnitListItem.kt
index ba5207f4..29fbb3d7 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/UnitListItem.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/components/UnitListItem.kt
@@ -18,11 +18,12 @@
package com.sadellie.unitto.feature.unitslist.components
+import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
-import androidx.compose.animation.with
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -53,8 +54,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.data.model.AbstractUnit
-import com.sadellie.unitto.feature.unitslist.R
/**
* Represents one list item. Once clicked will navigate up.
@@ -65,6 +66,7 @@ import com.sadellie.unitto.feature.unitslist.R
* @param favoriteAction Function to mark unit as favorite. It's a toggle.
* @param shortNameLabel String on the second line.
*/
+@SuppressLint("UnusedContentLambdaTargetStateParameter")
@Composable
private fun BasicUnitListItem(
modifier: Modifier,
@@ -124,7 +126,7 @@ private fun BasicUnitListItem(
),
targetState = isFavorite,
transitionSpec = {
- (scaleIn() with scaleOut()).using(SizeTransform(clip = false))
+ (scaleIn() togetherWith scaleOut()).using(SizeTransform(clip = false))
}
) {
Icon(
diff --git a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/navigation/UnitsListNavigation.kt b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/navigation/UnitsListNavigation.kt
index 8ad4d065..9c779305 100644
--- a/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/navigation/UnitsListNavigation.kt
+++ b/feature/unitslist/src/main/java/com/sadellie/unitto/feature/unitslist/navigation/UnitsListNavigation.kt
@@ -37,7 +37,7 @@ fun NavController.navigateToLeftSide(unitFromId: String) {
navigate("$leftSideRoute/$unitFromId")
}
-fun NavController.navigateToRightSide(unitFromId: String, unitToId: String, input: String) {
+fun NavController.navigateToRightSide(unitFromId: String, unitToId: String, input: String?) {
navigate("$rightSideRoute/$unitFromId/$unitToId/$input")
}
@@ -72,8 +72,8 @@ fun NavGraphBuilder.rightScreen(
) {
val unitFromId = it.arguments?.getString(unitFromIdArg) ?: return@composable
val unitToId = it.arguments?.getString(unitToIdArg) ?: return@composable
- val input = it.arguments?.getString(inputArg) ?: return@composable
- viewModel.setSelectedChip(unitFromId, false)
+ val input = it.arguments?.getString(inputArg)
+ viewModel.setSelectedChip(unitFromId, true)
RightSideScreen(
viewModel = viewModel,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f5ec2f09..66a7bbe4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,41 +1,39 @@
[versions]
-appCode = "20"
-appName = "Kobicha"
-kotlin = "1.8.10"
-androidxCore = "1.10.0"
-androidGradlePlugin = "7.4.2"
-orgJetbrainsKotlinxCoroutinesTest = "1.6.4"
+appCode = "22"
+appName = "Lilac Luster"
+kotlin = "1.9.0"
+androidxCore = "1.10.1"
+androidGradlePlugin = "8.0.2"
+orgJetbrainsKotlinxCoroutinesTest = "1.7.2"
androidxCompose = "1.5.0-alpha02"
-androidxComposeCompiler = "1.4.4"
-androidxComposeUi = "1.5.0-alpha02"
-androidxComposeMaterial3 = "1.1.0-beta02"
-androidxNavigation = "2.5.3"
+androidxComposeCompiler = "1.5.0"
+androidxComposeUi = "1.6.0-alpha01"
+androidxComposeMaterial3 = "1.2.0-alpha03"
+androidxNavigation = "2.6.0"
androidxLifecycleRuntimeCompose = "2.6.1"
androidxHilt = "1.0.0"
-comGoogleDagger = "2.45"
-androidxComposeMaterialIconsExtended = "1.5.0-alpha02"
+comGoogleDagger = "2.47"
+androidxComposeMaterialIconsExtended = "1.6.0-alpha01"
androidxDatastore = "1.0.0"
comGoogleAccompanist = "0.30.1"
-androidxRoom = "2.5.1"
-comSquareupMoshi = "1.14.0"
+androidxRoom = "2.6.0-alpha02"
+comSquareupMoshi = "1.15.0"
comSquareupRetrofit2 = "2.9.0"
-comGithubSadellieThemmo = "ed4063f70f"
+comGithubSadellieThemmo = "1.0.0"
orgBurnoutcrewComposereorderable = "0.9.6"
-comGithubSadellieExprk = "e55cba8f41"
-mxParser = "5.2.1"
-junit = "4.13.2"
+junit = "5.9.3"
androidxTest = "1.5.0"
-androidxTestExt = "1.1.4"
+androidxTestExt = "1.1.5"
androidDesugarJdkLibs = "2.0.3"
-androidxTestRunner = "1.5.1"
+androidxTestRunner = "1.5.2"
androidxTestRules = "1.5.0"
-orgRobolectric = "4.9"
+orgRobolectric = "4.10.3"
[libraries]
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
androidx-test = { group = "androidx.test", name = "core", version.ref = "androidxTest" }
androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" }
-junit = { group = "junit", name = "junit", version.ref = "junit" }
+junit = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
org-robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "orgRobolectric" }
@@ -62,9 +60,7 @@ com-squareup-moshi = { group = "com.squareup.moshi", name = "moshi-kotlin", vers
com-squareup-retrofit2 = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "comSquareupRetrofit2" }
com-github-sadellie-themmo = { group = "com.github.sadellie", name = "themmo", version.ref = "comGithubSadellieThemmo" }
org-burnoutcrew-composereorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "orgBurnoutcrewComposereorderable" }
-com-github-sadellie-exprk = { group = "com.github.sadellie", name = "ExprK", version.ref = "comGithubSadellieExprk" }
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
-org-mariuszgromada-math-mxparser = { group = "org.mariuszgromada.math", name = "MathParser.org-mXparser", version.ref = "mxParser" }
# classpath
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f16f7433..6c8fc19d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu Feb 02 22:43:30 AZT 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ffb6a0a7..761b20b9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -7,6 +7,7 @@ pluginManagement {
}
}
+@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
@@ -23,14 +24,14 @@ include(":core:ui")
include(":feature:converter")
include(":feature:unitslist")
include(":feature:calculator")
-include(":feature:settings")
-include(":feature:epoch")
+include(":feature:datedifference")
include(":feature:timezone")
+include(":feature:settings")
include(":data:userprefs")
-include(":data:unitgroups")
include(":data:licenses")
-include(":data:epoch")
+// include(":data:epoch")
include(":data:calculator")
include(":data:database")
include(":data:model")
include(":data:common")
+include(":data:evaluatto")