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 Google Play -F-Droid -Rustore +F-Droid ## 😎 Features - **Instant** expression evaluation @@ -29,25 +28,21 @@ - Customizable number **formatter** - **SI Standard** -## 👅 Translate -Unitto - Calculate and convert, but better. | POEditor +## 👅 [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 -Unitto - Calculate and convert, but better. | Product Hunt - -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")