Merge branch 'master' into feat/time-zone-converter
# Conflicts: # app/build.gradle.kts # app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt # core/base/src/main/res/values/strings.xml # settings.gradle.kts
@ -1,9 +0,0 @@
|
||||
<p align="middle">
|
||||
<img src="./content/donate.png" width="99%" />
|
||||
</p>
|
||||
|
||||
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
|
35
README.md
@ -11,8 +11,7 @@
|
||||
|
||||
## 📲 Download
|
||||
<a href="https://play.google.com/store/apps/details?id=com.sadellie.unitto"><img alt="Google Play" src="./content/googlePlay.png" width="32%"/></a>
|
||||
<a href="https://f-droid.org/packages/com.sadellie.unitto"><img alt="F-Droid" src="./content/fDroid.png" width="32%"/></a>
|
||||
<a href="https://apps.rustore.ru/app/com.sadellie.unitto"><img alt="Rustore" src="./content/ruStore.png" width="32%"/></a>
|
||||
<a href="https://f-droid.org/packages/com.sadellie.unitto"><img alt="F-Droid" src="./content/fDroid.png" width="27%"/></a>
|
||||
|
||||
## 😎 Features
|
||||
- **Instant** expression evaluation
|
||||
@ -29,25 +28,21 @@
|
||||
- Customizable number **formatter**
|
||||
- **SI Standard**
|
||||
|
||||
## 👅 Translate
|
||||
<a href="https://poeditor.com/join/project/T4zjmoq8dx" target="_blank"><img src="./content/poeditor.png" alt="Unitto - Calculate and convert, but better. | POEditor" style="width: 180px; height: 60px;" width="180" height="60" /></a>
|
||||
## 👅 [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
|
||||
<p align="middle">
|
||||
<a href="https://trello.com/b/cxAbRlvu/unitto" target="_blank">
|
||||
<img src="./content/progress.png" width="99%"/>
|
||||
</a>
|
||||
</p>
|
||||
Terms and Conditions: https://sadellie.github.io/unitto/terms
|
||||
|
||||
<a href="https://www.producthunt.com/posts/unitto?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-unitto" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=372851&theme=light" alt="Unitto - Calculate and convert, but better. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
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
|
||||
Privacy Policy: https://sadellie.github.io/unitto/privacy
|
||||
|
@ -16,6 +16,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().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")))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,12 @@ java {
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.android.gradlePlugin)
|
||||
compileOnly(libs.kotlin.gradlePlugin)
|
||||
|
@ -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<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
|
@ -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<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
|
@ -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<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
|
@ -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<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
|
@ -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<*, *, *, *>,
|
||||
) {
|
||||
|
@ -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<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
// Set JVM target to 11
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
build-logic/gradle/wrapper/gradle-wrapper.jar
vendored
@ -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
|
@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 50 KiB |
@ -16,6 +16,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
plugins {
|
||||
id("unitto.library")
|
||||
}
|
||||
|
@ -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<Int, Int> 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,
|
||||
)
|
||||
}
|
||||
|
@ -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<Int, Int> 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
|
||||
)
|
||||
}
|
||||
|
@ -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<Int, Int> by lazy {
|
||||
mapOf(
|
||||
Separator.SPACES to R.string.spaces,
|
||||
Separator.PERIOD to R.string.period,
|
||||
Separator.COMMA to R.string.comma
|
||||
)
|
||||
}
|
||||
|
@ -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<String, String> = 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<String> 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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 14 KiB |
@ -377,7 +377,7 @@
|
||||
<string name="attopascal_short">aPa</string>
|
||||
<string name="femtopascal">Femtopascal</string>
|
||||
<string name="femtopascal_short">fPa</string>
|
||||
<string name="picopascal">Picopascal</string>
|
||||
<string name="picopascal">Pikopascal</string>
|
||||
<string name="picopascal_short">pPa</string>
|
||||
<string name="nanopascal">Nanopascal</string>
|
||||
<string name="nanopascal_short">nPa</string>
|
||||
@ -421,7 +421,7 @@
|
||||
<string name="attometer_per_square_second_short">am/s^2</string>
|
||||
<string name="femtometer_per_square_second">Femtometer pro Quadratsekunde</string>
|
||||
<string name="femtometer_per_square_second_short">fm/s^2</string>
|
||||
<string name="picometer_per_square_second">Picometer pro Quadratsekunde</string>
|
||||
<string name="picometer_per_square_second">Pikometer pro Quadratsekunde</string>
|
||||
<string name="picometer_per_square_second_short">pm/s^2</string>
|
||||
<string name="nanometer_per_square_second">Nanometer pro Quadratsekunde</string>
|
||||
<string name="nanometer_per_square_second_short">mm/s^2</string>
|
||||
@ -659,7 +659,9 @@
|
||||
<string name="theme_setting">Farbthemen</string>
|
||||
<string name="precision_setting">Präzision</string>
|
||||
<string name="separator_setting">Trennzeichen</string>
|
||||
<string name="output_format_setting">Ausgabeformate</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="exponential_notation_setting">Exponentielle Notation</string>
|
||||
<string name="unit_groups_setting">Einheitengruppen</string>
|
||||
<string name="currency_rates_note_setting">Falsche Wechselkurse</string>
|
||||
<string name="currency_rates_note_title">Hinweis </string>
|
||||
@ -668,29 +670,29 @@
|
||||
<string name="privacy_policy">Datenschutz-Bestimmungen </string>
|
||||
<string name="third_party_licenses">Richtlinien von Drittanbietern </string>
|
||||
<string name="rate_this_app">Diese App bewerten</string>
|
||||
<string name="formatting_setting">Formatierung</string>
|
||||
<string name="additional_settings_group">Zusätzliches</string>
|
||||
|
||||
<!-- Precision -->
|
||||
<string name="precision_setting_support">Anzahl der Dezimalstellen</string>
|
||||
<string name="precision_setting_info">Umgerechnete Werte können eine höhere Präzision als eingestellt haben.</string>
|
||||
<string name="max_precision">1 000 (Maximum)</string>
|
||||
<string name="max_precision">%1$s (Maximum)</string>
|
||||
|
||||
<!-- Separator -->
|
||||
<string name="separator_setting_support">Gruppentrennzeichen</string>
|
||||
<string name="period">Punkt (42.069,12)</string>
|
||||
<string name="comma">Komma (42,069.12)</string>
|
||||
<string name="spaces">Leerzeichen (42 069.12)</string>
|
||||
<string name="period">Punkt</string>
|
||||
<string name="comma">Komma</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="space">Leerzeichen</string>
|
||||
|
||||
<!-- Output format -->
|
||||
<string name="output_format_setting_support">Ergebnisformatierung</string>
|
||||
<string name="output_format_setting_info">Wissenschaftliche Notation sieht wie 1E-21 aus</string>
|
||||
<string name="plain">Standard</string>
|
||||
<string name="allow_engineering">Wissenschaftliche Notation erlauben</string>
|
||||
<string name="force_engineering">Wissenschaftliche Notation erzwingen</string>
|
||||
<!-- Fuzzy -->
|
||||
<string name="exponential_notation_setting_support">Einen Teil der Nummer durch E ersetzen</string>
|
||||
|
||||
<!-- Theme -->
|
||||
<string name="theme_setting_support">App Aussehen</string>
|
||||
<string name="force_auto_mode">Automatisch</string>
|
||||
<string name="auto_label">Automatisch</string>
|
||||
<string name="force_light_mode">Hell</string>
|
||||
<string name="force_dark_mode">Dunkel</string>
|
||||
<string name="color_theme">Farbthema</string>
|
||||
@ -726,4 +728,415 @@
|
||||
<string name="reorder_unit_group_description">Einheitengruppe umordnen</string>
|
||||
<string name="disable_unit_group_description">Einheitengruppe deaktivieren</string>
|
||||
<string name="app_version_name_setting">Versionsname</string>
|
||||
<string name="about_unitto">Über Unitto</string>
|
||||
<string name="about_unitto_support">Mehr über die App erfahren</string>
|
||||
<string name="unit_groups_support">Einheiten deaktivieren und anordnen</string>
|
||||
<string name="cent">Cent</string>
|
||||
<string name="cent_short">cent</string>
|
||||
|
||||
<!-- Flux -->
|
||||
<string name="maxwell">Maxwell</string>
|
||||
<string name="maxwell_short">Mx</string>
|
||||
<string name="weber">Weber</string>
|
||||
<string name="weber_short">Wb</string>
|
||||
<string name="milliweber">Milliweber</string>
|
||||
<string name="milliweber_short">mWb</string>
|
||||
<string name="microweber">Microweber</string>
|
||||
<string name="microweber_short">μWb</string>
|
||||
<string name="kiloweber">Kiloweber</string>
|
||||
<string name="kiloweber_short">kWb</string>
|
||||
<string name="megaweber">Megaweber</string>
|
||||
<string name="megaweber_short">MWb</string>
|
||||
<string name="gigaweber">Gigaweber</string>
|
||||
<string name="gigaweber_short">GWb</string>
|
||||
<string name="flux">Flux</string>
|
||||
<string name="open_source">Quellcode anzeigen</string>
|
||||
<string name="translate_app">Diese App übersetzen</string>
|
||||
<string name="translate_app_support">Dem POEditor-Projekt beitreten und helfen</string>
|
||||
|
||||
<!-- Number base -->
|
||||
<string name="binary">Binär</string>
|
||||
<string name="binary_short">base2</string>
|
||||
<string name="ternary">Ternär</string>
|
||||
<string name="ternary_short">base3</string>
|
||||
<string name="quaternary">Quartär</string>
|
||||
<string name="quaternary_short">base4</string>
|
||||
<string name="quinary">Quinär</string>
|
||||
<string name="quinary_short">base5</string>
|
||||
<string name="senary">Senär</string>
|
||||
<string name="senary_short">base6</string>
|
||||
<string name="septenary">Septenär</string>
|
||||
<string name="septenary_short">base7</string>
|
||||
<string name="octal">Oktal</string>
|
||||
<string name="octal_short">base8</string>
|
||||
<string name="nonary">Nonär</string>
|
||||
<string name="nonary_short">base9</string>
|
||||
<string name="decimal">Dezimal</string>
|
||||
<string name="decimal_short">base10</string>
|
||||
<string name="undecimal">Undezimal</string>
|
||||
<string name="undecimal_short">base11</string>
|
||||
<string name="duodecimal">Duodezimal</string>
|
||||
<string name="duodecimal_short">base12</string>
|
||||
<string name="tridecimal">Tridezimal</string>
|
||||
<string name="tridecimal_short">base13</string>
|
||||
<string name="tetradecimal">Tetradezimal</string>
|
||||
<string name="tetradecimal_short">base14</string>
|
||||
<string name="pentadecimal">Pentadezimal</string>
|
||||
<string name="pentadecimal_short">base15</string>
|
||||
<string name="hexadecimal">Hexadezimal</string>
|
||||
<string name="hexadecimal_short">base16</string>
|
||||
<string name="number_base">Basis</string>
|
||||
<string name="enable_vibrations">Vibrationen</string>
|
||||
<string name="enable_vibrations_support">Haptisches Feedback für Tastaturtasten</string>
|
||||
<string name="millibar">Millibar</string>
|
||||
<string name="millibar_short">mbar</string>
|
||||
<string name="kilopascal">Kilopascal</string>
|
||||
<string name="kilopascal_short">kPa</string>
|
||||
<string name="micron_of_mercury">Mikrometer Quecksilber</string>
|
||||
<string name="micron_of_mercury_short">μmHg</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<!-- Fuzzy -->
|
||||
<string name="epoch_converter">Epochkonverter</string>
|
||||
|
||||
<!-- Calculator -->
|
||||
<string name="calculator">Taschenrechner</string>
|
||||
|
||||
<!-- Epoch -->
|
||||
<string name="year_short">y</string>
|
||||
<string name="month_short">m</string>
|
||||
<string name="nautical_mile">Nautische Meile</string>
|
||||
<string name="nautical_mile_short">M</string>
|
||||
<string name="starting_screen_setting">Startbildschirm</string>
|
||||
<string name="starting_screen_setting_support">Wählen Sie, welcher Bildschirm beim Starten der App angezeigt wird</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<string name="unit_converter">Einheitenumrechner</string>
|
||||
<string name="calculator_clear_history_label">Leeren</string>
|
||||
<string name="calculator_clear_history_title">Verlauf leeren</string>
|
||||
<string name="calculator_clear_history_support">Alle Ausdrücke aus dem Verlauf werden für immer gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="calculator_no_history">Kein Verlauf</string>
|
||||
<string name="open_menu_description">Menü öffnen</string>
|
||||
<string name="microgram">Mikrogramm</string>
|
||||
<string name="microgram_short">µg</string>
|
||||
|
||||
<!-- ELECTROSTATIC CAPACITANCE -->
|
||||
<string name="attofarad">Attofarad</string>
|
||||
<string name="attofarad_short">aF</string>
|
||||
<string name="statfarad">Statfarad</string>
|
||||
<string name="statfarad_short">stF</string>
|
||||
<string name="farad">Farad</string>
|
||||
<string name="farad_short">F</string>
|
||||
<string name="exafarad">Exafarad</string>
|
||||
<string name="exafarad_short">EF</string>
|
||||
<string name="picofarad">Pikofarad</string>
|
||||
<string name="picofarad_short">pF</string>
|
||||
<string name="nanofarad">Nanofarad</string>
|
||||
<string name="nanofarad_short">nF</string>
|
||||
<string name="microfarad">Microfarad</string>
|
||||
<string name="microfarad_short">µF</string>
|
||||
<string name="millifarad">Millifarad</string>
|
||||
<string name="millifarad_short">mF</string>
|
||||
<string name="kilofarad">Kilofarad</string>
|
||||
<string name="kilofarad_short">kF</string>
|
||||
<string name="megafarad">Megafarad</string>
|
||||
<string name="megafarad_short">MF</string>
|
||||
<string name="gigafarad">Gigafarad</string>
|
||||
<string name="gigafarad_short">GF</string>
|
||||
<string name="petafarad">Petafarad</string>
|
||||
<string name="petafarad_short">PF</string>
|
||||
|
||||
<!-- Prefixes -->
|
||||
<string name="prefix_quetta">Quetta</string>
|
||||
<string name="prefix_quetta_short">Q</string>
|
||||
<string name="prefix_ronna">Ronna</string>
|
||||
<string name="prefix_ronna_short">R</string>
|
||||
<string name="prefix_yotta">Yotta</string>
|
||||
<string name="prefix_yotta_short">Y</string>
|
||||
<string name="prefix_zetta">Zetta</string>
|
||||
<string name="prefix_zetta_short">Z</string>
|
||||
<string name="prefix_exa">Exa</string>
|
||||
<string name="prefix_exa_short">E</string>
|
||||
<string name="prefix_peta">Peta</string>
|
||||
<string name="prefix_peta_short">P</string>
|
||||
<string name="prefix_tera">Tera</string>
|
||||
<string name="prefix_tera_short">T</string>
|
||||
<string name="prefix_giga">Giga</string>
|
||||
<string name="prefix_giga_short">G</string>
|
||||
<string name="prefix_mega">Mega</string>
|
||||
<string name="prefix_mega_short">M</string>
|
||||
<string name="prefix_kilo">Kilo</string>
|
||||
<string name="prefix_kilo_short">k</string>
|
||||
<string name="prefix_hecto">Hekto</string>
|
||||
<string name="prefix_hecto_short">h</string>
|
||||
<string name="prefix_deca">Deka</string>
|
||||
<string name="prefix_deca_short">da</string>
|
||||
<string name="prefix_base">Basis</string>
|
||||
<string name="prefix_base_short">Basis</string>
|
||||
<string name="prefix_deci">Dezi</string>
|
||||
<string name="prefix_deci_short">d</string>
|
||||
<string name="prefix_centi">Zenti</string>
|
||||
<string name="prefix_centi_short">c</string>
|
||||
<string name="prefix_milli">Milli</string>
|
||||
<string name="prefix_milli_short">m</string>
|
||||
<string name="prefix_micro">Micro</string>
|
||||
<string name="prefix_micro_short">μ</string>
|
||||
<string name="prefix_nano">Nano</string>
|
||||
<string name="prefix_nano_short">n</string>
|
||||
<string name="prefix_pico">Piko</string>
|
||||
<string name="prefix_pico_short">P</string>
|
||||
<string name="prefix_femto">Femto</string>
|
||||
<string name="prefix_femto_short">f</string>
|
||||
<string name="prefix_atto">Atto</string>
|
||||
<string name="prefix_atto_short">a</string>
|
||||
<string name="prefix_zepto">Zepto</string>
|
||||
<string name="prefix_zepto_short">z</string>
|
||||
<string name="prefix_yocto">Yokto</string>
|
||||
<string name="prefix_yocto_short">y</string>
|
||||
<string name="prefix_ronto">Ronto</string>
|
||||
<string name="prefix_ronto_short">r</string>
|
||||
<string name="prefix_quecto">Quekto</string>
|
||||
<string name="prefix_quecto_short">q</string>
|
||||
|
||||
<!-- Force -->
|
||||
<string name="newton">Newton</string>
|
||||
<string name="newton_short">N</string>
|
||||
<string name="kilonewton">Kilonewton</string>
|
||||
<string name="kilonewton_short">kN</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="gram_force">Gramm-Kraft</string>
|
||||
<string name="gram_force_short">gf</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="kilogram_force">Kilogramm-Kraft</string>
|
||||
<string name="kilogram_force_short">kgf</string>
|
||||
<string name="ton_force">Tonnen-Kraft</string>
|
||||
<string name="ton_force_short">tf</string>
|
||||
<string name="millinewton">Millinewton</string>
|
||||
<string name="millinewton_short">mN</string>
|
||||
<string name="attonewton">Attonewton</string>
|
||||
<string name="attonewton_short">aN</string>
|
||||
<string name="dyne">Dyn</string>
|
||||
<string name="dyne_short">dyn</string>
|
||||
<string name="joule_per_meter">Joule/Meter</string>
|
||||
<string name="joule_per_meter_short">J/m</string>
|
||||
<string name="joule_per_centimeter">Joule/Zentimeter</string>
|
||||
<string name="joule_per_centimeter_short">J/cm</string>
|
||||
<string name="kilopound_force">Kilopfund-Kraft</string>
|
||||
<string name="kilopound_force_short">kipf</string>
|
||||
<string name="pound_force">Pfund-Kraft</string>
|
||||
<string name="pound_force_short">lbf</string>
|
||||
<string name="ounce_force">Unzen-Kraft</string>
|
||||
<string name="ounce_force_short">ozf</string>
|
||||
<string name="pond">Pond</string>
|
||||
<string name="pond_short">p</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="kilopond">Kilopond</string>
|
||||
<string name="kilopond_short">kp</string>
|
||||
|
||||
<!-- Torque -->
|
||||
<string name="newton_meter">Newtonmeter</string>
|
||||
<string name="newton_meter_short">N*m</string>
|
||||
<string name="newton_centimeter">Newtonzentimeter</string>
|
||||
<string name="newton_centimeter_short">N*cm</string>
|
||||
<string name="newton_millimeter">Newtonmillimeter</string>
|
||||
<string name="newton_millimeter_short">N*mm</string>
|
||||
<string name="kilonewton_meter">Kilonewtonmeter</string>
|
||||
<string name="kilonewton_meter_short">kN*m</string>
|
||||
<string name="dyne_meter">Dynmeter</string>
|
||||
<string name="dyne_meter_short">dyn*m</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="dyne_centimeter">Dynzentimeter</string>
|
||||
<string name="dyne_centimeter_short">dyn*cm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="dyne_millimeter">Dynmillimeter</string>
|
||||
<string name="dyne_millimeter_short">dyn*mm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="kilogram_force_meter">Kilogramm-Kraft-Meter</string>
|
||||
<string name="kilogram_force_meter_short">kgf*m</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="kilogram_force_centimeter">Kilogramm-Kraft-Zentimeter</string>
|
||||
<string name="kilogram_force_centimeter_short">kgf*cm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="kilogram_force_millimeter">Kilogramm-Kraft-Millimeter</string>
|
||||
<string name="kilogram_force_millimeter_short">kgf*mm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="gram_force_meter">Gramm-Kraft Meter</string>
|
||||
<string name="gram_force_meter_short">gf*m</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="gram_force_centimeter">Gramm-Kraft Zentimeter</string>
|
||||
<string name="gram_force_centimeter_short">gf*cm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="gram_force_millimeter">Gramm-Kraft Millimeter</string>
|
||||
<string name="gram_force_millimeter_short">gf*mm</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="ounce_force_foot">Unzen-Kraft-Fuß</string>
|
||||
<string name="ounce_force_foot_short">ozf*ft</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="ounce_force_inch">Unze-Kraft-Zoll</string>
|
||||
<string name="ounce_force_inch_short">ozf*in</string>
|
||||
<string name="pound_force_foot">Pfund-Kraft-Fuß</string>
|
||||
<string name="pound_force_foot_short">lbf*ft</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="pound_force_inch">Pfund-Kraft-Zoll</string>
|
||||
<string name="pound_force_inch_short">lbf*in</string>
|
||||
|
||||
<!-- Flow rate -->
|
||||
<string name="liter_per_hour">Liter/Stunde</string>
|
||||
<string name="liter_per_hour_short">L/h</string>
|
||||
<string name="liter_per_minute">Liter/Minute</string>
|
||||
<string name="liter_per_minute_short">L/m</string>
|
||||
<string name="liter_per_second">Liter/Sekunde</string>
|
||||
<string name="liter_per_second_short">L/s</string>
|
||||
<string name="milliliter_per_hour">Milliliter/Stunde</string>
|
||||
<string name="milliliter_per_hour_short">mL/h</string>
|
||||
<string name="milliliter_per_minute">Milliliter/Minute</string>
|
||||
<string name="milliliter_per_minute_short">mL/m</string>
|
||||
<string name="milliliter_per_second">Milliliter/Sekunde</string>
|
||||
<string name="milliliter_per_second_short">mL/s</string>
|
||||
<string name="cubic_meter_per_hour">Kubikmeter/Stunde</string>
|
||||
<string name="cubic_meter_per_hour_short">m3/h</string>
|
||||
<string name="cubic_meter_per_minute">Kubikmeter/Minute</string>
|
||||
<string name="cubic_meter_per_minute_short">m3/m</string>
|
||||
<string name="cubic_meter_per_second">Kubikmeter/Sekunde</string>
|
||||
<string name="cubic_meter_per_second_short">m3/s</string>
|
||||
<string name="cubic_millimeter_per_hour">Kubikmillimeter/Stunde</string>
|
||||
<string name="cubic_millimeter_per_hour_short">mm3/h</string>
|
||||
<string name="cubic_millimeter_per_minute">Kubikmillimeter/Minute</string>
|
||||
<string name="cubic_millimeter_per_minute_short">mm3/m</string>
|
||||
<string name="cubic_millimeter_per_second">Kubikmillimeter/Sekunde</string>
|
||||
<string name="cubic_millimeter_per_second_short">mm3/s</string>
|
||||
<string name="cubic_foot_per_hour">Kubikfuß/Stunde</string>
|
||||
<string name="cubic_foot_per_hour_short">ft3/h</string>
|
||||
<string name="cubic_foot_per_minute">Kubikfuß/Minute</string>
|
||||
<string name="cubic_foot_per_minute_short">ft3/m</string>
|
||||
<string name="cubic_foot_per_second">Kubikfuß/Sekunde</string>
|
||||
<string name="cubic_foot_per_second_short">ft3/s</string>
|
||||
<string name="gallon_per_hour_us">Gallone/Stunde (U.S.)</string>
|
||||
<string name="gallon_per_hour_us_short">gal/h</string>
|
||||
<string name="gallon_per_minute_us">Gallone/Minute(U.S.)</string>
|
||||
<string name="gallon_per_minute_us_short">gal/m</string>
|
||||
<string name="gallon_per_second_us">Gallone/Sekunde(U.S.)</string>
|
||||
<string name="gallon_per_second_us_short">gal/s</string>
|
||||
<string name="gallon_per_hour_imperial">Gallone/Stunde (Imperial)</string>
|
||||
<string name="gallon_per_hour_imperial_short">gal/h</string>
|
||||
<string name="gallon_per_minute_imperial">Gallone/Minute(Imperial)</string>
|
||||
<string name="gallon_per_minute_imperial_short">gal/m</string>
|
||||
<string name="gallon_per_second_imperial">Gallone/Sekunde(Imperial)</string>
|
||||
<string name="gallon_per_second_imperial_short">gal/s</string>
|
||||
|
||||
<!-- Luminance -->
|
||||
<string name="candela_per_square_meter">Candela/Quadratmeter</string>
|
||||
<string name="candela_per_square_meter_short">cd/m^2</string>
|
||||
<string name="candela_per_square_centimeter">Candela/Quadratzentimeter</string>
|
||||
<string name="candela_per_square_centimeter_short">cd/cm^2</string>
|
||||
<string name="candela_per_square_foot">Candela/Quadratfuß</string>
|
||||
<string name="candela_per_square_foot_short">cd/ft^2</string>
|
||||
<string name="candela_per_square_inch">Candela/Quadratzoll</string>
|
||||
<string name="candela_per_square_inch_short">cd/in^2</string>
|
||||
<string name="kilocandela_per_square_meter">Kilocandela/Quadratmeter</string>
|
||||
<string name="kilocandela_per_square_meter_short">kcd</string>
|
||||
<string name="stilb">Stilb</string>
|
||||
<string name="stilb_short">sb</string>
|
||||
<string name="lumen_per_square_meter_per_steradian">Lumen/Quadratmeter/Steradiant</string>
|
||||
<string name="lumen_per_square_meter_per_steradian_short">lm/m^2/sr</string>
|
||||
<string name="lumen_per_square_centimeter_per_steradian">Lumen/Quadratzentimeter/Steradian</string>
|
||||
<string name="lumen_per_square_centimeter_per_steradian_short">lm/cm^2/sr</string>
|
||||
<string name="lumen_per_square_foot_per_steradian">Lumen/Quadratfuß/Steradian</string>
|
||||
<string name="lumen_per_square_foot_per_steradian_short">lm/ft^2/sr</string>
|
||||
<string name="watt_per_square_centimeter_per_steradian">Watt/Quadratzentimeter/Steradian</string>
|
||||
<string name="watt_per_square_centimeter_per_steradian_short">W/cm^2/sr</string>
|
||||
<string name="nit">Nit</string>
|
||||
<string name="nit_short">nt</string>
|
||||
<string name="millinit">Millinit</string>
|
||||
<string name="millinit_short">mnt</string>
|
||||
<string name="lambert">Lambert</string>
|
||||
<string name="lambert_short">L</string>
|
||||
<string name="millilambert">Millilambert</string>
|
||||
<string name="millilambert_short">mL</string>
|
||||
<string name="foot_lambert">Fuß-Lambert</string>
|
||||
<string name="foot_lambert_short">fL</string>
|
||||
<string name="apostilb">Apostilb</string>
|
||||
<string name="apostilb_short">asb</string>
|
||||
<string name="blondel">Blondel</string>
|
||||
<string name="blondel_short">blondel</string>
|
||||
<string name="bril">Bril</string>
|
||||
<string name="bril_short">bril</string>
|
||||
<string name="skot">Skot</string>
|
||||
<string name="skot_short">sk</string>
|
||||
<string name="electrostatic_capacitance">Kapazität</string>
|
||||
<string name="prefix">Präfix</string>
|
||||
<string name="force">Kraft</string>
|
||||
<string name="torque">Drehmoment</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="flow_rate">Fluss</string>
|
||||
<string name="luminance">Leuchtdichte</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="format_time">Zeit formatieren</string>
|
||||
<string name="format_time_support">Beispiel: 130 Minuten als 2h 10m anzeigen</string>
|
||||
<string name="units_sorting">Einheitenlistensortierung</string>
|
||||
<string name="units_sorting_support">Einheitenreihenfolge ändern</string>
|
||||
|
||||
<!-- Units list sorting -->
|
||||
<string name="sort_by_usage">Benutzung</string>
|
||||
<string name="sort_by_alphabetical">Alphabetisch</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="sort_by_scale_desc">Skala (Abst.)</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="sort_by_scale_asc">Skala (Aufst.)</string>
|
||||
<string name="color_theme_support">Farbthema auswählen</string>
|
||||
<string name="color_scheme">Farbschema</string>
|
||||
<string name="selected_color">Ausgewählte Farbe</string>
|
||||
<string name="monet_mode">Stil auswählen</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
|
||||
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
|
||||
|
||||
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
|
||||
<string name="formatting_setting_support">Genauigkeit und Zahlendarstellung</string>
|
||||
<string name="divide_by_zero_error">Kann nicht durch 0 teilen</string>
|
||||
<string name="date_difference">Datumsunterschied</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
|
||||
<string name="select_time">Zeit auswählen</string>
|
||||
<string name="date_difference_start">Anfang</string>
|
||||
<string name="date_difference_end">Ende</string>
|
||||
<string name="date_difference_result">Unterschied</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_years">Jahre</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_months">Monate</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_days">Tage</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_hours">Stunden</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_minutes">Minuten</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
|
||||
|
||||
Used in this dialog window. Should be short -->
|
||||
<string name="next_label">Weiter</string>
|
||||
</resources>
|
@ -228,11 +228,11 @@
|
||||
<string name="exabyte_per_second_short">EB/s</string>
|
||||
|
||||
<!-- Volume -->
|
||||
<string name="attoliter">Attoliter</string>
|
||||
<string name="attoliter">Attolitre</string>
|
||||
<string name="attoliter_short">aL</string>
|
||||
<string name="milliliter">Milliliter</string>
|
||||
<string name="milliliter">Millilitre</string>
|
||||
<string name="milliliter_short">mL</string>
|
||||
<string name="liter">Liter</string>
|
||||
<string name="liter">Litre</string>
|
||||
<string name="liter_short">L</string>
|
||||
<string name="us_liquid_gallon">US liquid gallon</string>
|
||||
<string name="us_liquid_gallon_short">gal (US)</string>
|
||||
@ -659,10 +659,8 @@
|
||||
<string name="theme_setting">Themes</string>
|
||||
<string name="precision_setting">Precision</string>
|
||||
<string name="separator_setting">Separator</string>
|
||||
<string name="output_format_setting">Output format</string>
|
||||
<string name="exponential_notation_setting">Exponential notation</string>
|
||||
<string name="unit_groups_setting">Unit groups</string>
|
||||
<string name="enable_vibrations">Vibrations</string>
|
||||
<string name="enable_vibrations_support">Haptic feedback when clicking keyboard buttons</string>
|
||||
<string name="currency_rates_note_setting">Wrong currency rates?</string>
|
||||
<string name="currency_rates_note_title">Note</string>
|
||||
<string name="currency_rates_note_text">Currency rates are updated daily. There\'s no real-time market monitoring in the app</string>
|
||||
@ -670,30 +668,26 @@
|
||||
<string name="privacy_policy">Privacy Policy</string>
|
||||
<string name="third_party_licenses">Third party licenses</string>
|
||||
<string name="rate_this_app">Rate this app</string>
|
||||
<string name="formatting_settings_group">Formatting</string>
|
||||
<string name="formatting_setting">Formatting</string>
|
||||
<string name="additional_settings_group">Additional</string>
|
||||
|
||||
<!-- Precision -->
|
||||
<string name="precision_setting_support">Number of decimal places</string>
|
||||
<string name="precision_setting_info">Converted values may have a precision higher than the preferred one.</string>
|
||||
<string name="max_precision">1 000 (Max)</string>
|
||||
<string name="max_precision">%1$s (Max)</string>
|
||||
|
||||
<!-- Separator -->
|
||||
<string name="separator_setting_support">Group separator symbol</string>
|
||||
<string name="period">Period (42.069,12)</string>
|
||||
<string name="comma">Comma (42,069.12)</string>
|
||||
<string name="spaces">Spaces (42 069.12)</string>
|
||||
<string name="period">Period</string>
|
||||
<string name="comma">Comma</string>
|
||||
<string name="space">Space</string>
|
||||
|
||||
<!-- Output format -->
|
||||
<string name="output_format_setting_support">Result value formatting</string>
|
||||
<string name="output_format_setting_info">Engineering strings look like 1E-21</string>
|
||||
<string name="plain">Default</string>
|
||||
<string name="allow_engineering">Allow engineering</string>
|
||||
<string name="force_engineering">Force engineering</string>
|
||||
<string name="exponential_notation_setting_support">Replace part of the number with E</string>
|
||||
|
||||
<!-- Theme -->
|
||||
<string name="theme_setting_support">App look and feel</string>
|
||||
<string name="force_auto_mode">Auto</string>
|
||||
<string name="auto_label">Auto</string>
|
||||
<string name="force_light_mode">Light</string>
|
||||
<string name="force_dark_mode">Dark</string>
|
||||
<string name="color_theme">Colour theme</string>
|
||||
@ -787,4 +781,321 @@
|
||||
<string name="hexadecimal">Hexadecimal</string>
|
||||
<string name="hexadecimal_short">base16</string>
|
||||
<string name="number_base">Base</string>
|
||||
<string name="enable_vibrations">Vibrations</string>
|
||||
<string name="enable_vibrations_support">Haptic feedback when clicking keyboard buttons</string>
|
||||
<string name="millibar">Millibar</string>
|
||||
<string name="millibar_short">mbar</string>
|
||||
<string name="kilopascal">Kilopascal</string>
|
||||
<string name="kilopascal_short">kPa</string>
|
||||
<string name="micron_of_mercury">Micron of mercury</string>
|
||||
<string name="micron_of_mercury_short">μmHg</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<string name="epoch_converter">Epoch converter</string>
|
||||
|
||||
<!-- Calculator -->
|
||||
<string name="calculator">Calculator</string>
|
||||
|
||||
<!-- Epoch -->
|
||||
<string name="year_short">y</string>
|
||||
<string name="month_short">m</string>
|
||||
<string name="nautical_mile">Nautical mile</string>
|
||||
<string name="nautical_mile_short">M</string>
|
||||
<string name="starting_screen_setting">Starting screen</string>
|
||||
<string name="starting_screen_setting_support">Choose which screen is shown when you launch the app</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<string name="unit_converter">Unit converter</string>
|
||||
<string name="calculator_clear_history_label">Clear</string>
|
||||
<string name="calculator_clear_history_title">Clear history</string>
|
||||
<string name="calculator_clear_history_support">All expressions from history will be deleted forever. This action can\'t be undone!</string>
|
||||
<string name="calculator_no_history">No history</string>
|
||||
<string name="open_menu_description">Open menu</string>
|
||||
<string name="microgram">Microgram</string>
|
||||
<string name="microgram_short">µg</string>
|
||||
|
||||
<!-- ELECTROSTATIC CAPACITANCE -->
|
||||
<string name="attofarad">Attofarad</string>
|
||||
<string name="attofarad_short">aF</string>
|
||||
<string name="statfarad">Statfarad</string>
|
||||
<string name="statfarad_short">stF</string>
|
||||
<string name="farad">Farad</string>
|
||||
<string name="farad_short">F</string>
|
||||
<string name="exafarad">Exafarad</string>
|
||||
<string name="exafarad_short">EF</string>
|
||||
<string name="picofarad">Picofarad</string>
|
||||
<string name="picofarad_short">pF</string>
|
||||
<string name="nanofarad">Nanofarad</string>
|
||||
<string name="nanofarad_short">nF</string>
|
||||
<string name="microfarad">Microfarad</string>
|
||||
<string name="microfarad_short">µF</string>
|
||||
<string name="millifarad">Millifarad</string>
|
||||
<string name="millifarad_short">mF</string>
|
||||
<string name="kilofarad">Kilofarad</string>
|
||||
<string name="kilofarad_short">kF</string>
|
||||
<string name="megafarad">Megafarad</string>
|
||||
<string name="megafarad_short">MF</string>
|
||||
<string name="gigafarad">Gigafarad</string>
|
||||
<string name="gigafarad_short">GF</string>
|
||||
<string name="petafarad">Petafarad</string>
|
||||
<string name="petafarad_short">PF</string>
|
||||
|
||||
<!-- Prefixes -->
|
||||
<string name="prefix_quetta">Quetta</string>
|
||||
<string name="prefix_quetta_short">Q</string>
|
||||
<string name="prefix_ronna">Ronna</string>
|
||||
<string name="prefix_ronna_short">R</string>
|
||||
<string name="prefix_yotta">Yotta</string>
|
||||
<string name="prefix_yotta_short">Y</string>
|
||||
<string name="prefix_zetta">Zetta</string>
|
||||
<string name="prefix_zetta_short">Z</string>
|
||||
<string name="prefix_exa">Exa</string>
|
||||
<string name="prefix_exa_short">E</string>
|
||||
<string name="prefix_peta">Peta</string>
|
||||
<string name="prefix_peta_short">P</string>
|
||||
<string name="prefix_tera">Tera</string>
|
||||
<string name="prefix_tera_short">T</string>
|
||||
<string name="prefix_giga">Giga</string>
|
||||
<string name="prefix_giga_short">G</string>
|
||||
<string name="prefix_mega">Mega</string>
|
||||
<string name="prefix_mega_short">M</string>
|
||||
<string name="prefix_kilo">Kilo</string>
|
||||
<string name="prefix_kilo_short">k</string>
|
||||
<string name="prefix_hecto">Hecto</string>
|
||||
<string name="prefix_hecto_short">h</string>
|
||||
<string name="prefix_deca">Deca</string>
|
||||
<string name="prefix_deca_short">da</string>
|
||||
<string name="prefix_base">Base</string>
|
||||
<string name="prefix_base_short">Base</string>
|
||||
<string name="prefix_deci">Deci</string>
|
||||
<string name="prefix_deci_short">d</string>
|
||||
<string name="prefix_centi">Centi</string>
|
||||
<string name="prefix_centi_short">c</string>
|
||||
<string name="prefix_milli">Milli</string>
|
||||
<string name="prefix_milli_short">m</string>
|
||||
<string name="prefix_micro">Micro</string>
|
||||
<string name="prefix_micro_short">μ</string>
|
||||
<string name="prefix_nano">Nano</string>
|
||||
<string name="prefix_nano_short">n</string>
|
||||
<string name="prefix_pico">Pico</string>
|
||||
<string name="prefix_pico_short">p</string>
|
||||
<string name="prefix_femto">Femto</string>
|
||||
<string name="prefix_femto_short">f</string>
|
||||
<string name="prefix_atto">Atto</string>
|
||||
<string name="prefix_atto_short">a</string>
|
||||
<string name="prefix_zepto">Zepto</string>
|
||||
<string name="prefix_zepto_short">z</string>
|
||||
<string name="prefix_yocto">Yocto</string>
|
||||
<string name="prefix_yocto_short">y</string>
|
||||
<string name="prefix_ronto">Ronto</string>
|
||||
<string name="prefix_ronto_short">r</string>
|
||||
<string name="prefix_quecto">Quecto</string>
|
||||
<string name="prefix_quecto_short">q</string>
|
||||
|
||||
<!-- Force -->
|
||||
<string name="newton">Newton</string>
|
||||
<string name="newton_short">N</string>
|
||||
<string name="kilonewton">Kilonewton</string>
|
||||
<string name="kilonewton_short">kN</string>
|
||||
<string name="gram_force">Gram-force</string>
|
||||
<string name="gram_force_short">gf</string>
|
||||
<string name="kilogram_force">Kilogram-force</string>
|
||||
<string name="kilogram_force_short">kgf</string>
|
||||
<string name="ton_force">Ton-force</string>
|
||||
<string name="ton_force_short">tf</string>
|
||||
<string name="millinewton">Millinewton</string>
|
||||
<string name="millinewton_short">mN</string>
|
||||
<string name="attonewton">Attonewton</string>
|
||||
<string name="attonewton_short">aN</string>
|
||||
<string name="dyne">Dyne</string>
|
||||
<string name="dyne_short">dyn</string>
|
||||
<string name="joule_per_meter">Joule/metre</string>
|
||||
<string name="joule_per_meter_short">J/m</string>
|
||||
<string name="joule_per_centimeter">Joule/centimetre</string>
|
||||
<string name="joule_per_centimeter_short">J/cm</string>
|
||||
<string name="kilopound_force">Kilopound-force</string>
|
||||
<string name="kilopound_force_short">kipf</string>
|
||||
<string name="pound_force">Pound-force</string>
|
||||
<string name="pound_force_short">lbf</string>
|
||||
<string name="ounce_force">Ounce-force</string>
|
||||
<string name="ounce_force_short">ozf</string>
|
||||
<string name="pond">Pond</string>
|
||||
<string name="pond_short">p</string>
|
||||
<string name="kilopond">Kilopond</string>
|
||||
<string name="kilopond_short">kp</string>
|
||||
|
||||
<!-- Torque -->
|
||||
<string name="newton_meter">Newton metre</string>
|
||||
<string name="newton_meter_short">N*m</string>
|
||||
<string name="newton_centimeter">Newton centimetre</string>
|
||||
<string name="newton_centimeter_short">N*cm</string>
|
||||
<string name="newton_millimeter">Newton millimetre</string>
|
||||
<string name="newton_millimeter_short">N*mm</string>
|
||||
<string name="kilonewton_meter">Kilonewton metre</string>
|
||||
<string name="kilonewton_meter_short">kN*m</string>
|
||||
<string name="dyne_meter">Dyne metre</string>
|
||||
<string name="dyne_meter_short">dyn*m</string>
|
||||
<string name="dyne_centimeter">Dyne centimetre</string>
|
||||
<string name="dyne_centimeter_short">dyn*cm</string>
|
||||
<string name="dyne_millimeter">Dyne millimetre</string>
|
||||
<string name="dyne_millimeter_short">dyn*mm</string>
|
||||
<string name="kilogram_force_meter">Kilogram-force metre</string>
|
||||
<string name="kilogram_force_meter_short">kgf*m</string>
|
||||
<string name="kilogram_force_centimeter">Kilogram-force centimetre</string>
|
||||
<string name="kilogram_force_centimeter_short">kgf*cm</string>
|
||||
<string name="kilogram_force_millimeter">Kilogram-force millimetre</string>
|
||||
<string name="kilogram_force_millimeter_short">kgf*mm</string>
|
||||
<string name="gram_force_meter">Gram-force metre</string>
|
||||
<string name="gram_force_meter_short">gf*m</string>
|
||||
<string name="gram_force_centimeter">Gram-force centimetre</string>
|
||||
<string name="gram_force_centimeter_short">gf*cm</string>
|
||||
<string name="gram_force_millimeter">Gram-force millimetre</string>
|
||||
<string name="gram_force_millimeter_short">gf*mm</string>
|
||||
<string name="ounce_force_foot">Ounce-force foot</string>
|
||||
<string name="ounce_force_foot_short">ozf*ft</string>
|
||||
<string name="ounce_force_inch">Ounce-force inch</string>
|
||||
<string name="ounce_force_inch_short">ozf*in</string>
|
||||
<string name="pound_force_foot">Pound-force foot</string>
|
||||
<string name="pound_force_foot_short">lbf*ft</string>
|
||||
<string name="pound_force_inch">Pound-force inch</string>
|
||||
<string name="pound_force_inch_short">lbf*in</string>
|
||||
|
||||
<!-- Flow rate -->
|
||||
<string name="liter_per_hour">Litre/hour</string>
|
||||
<string name="liter_per_hour_short">L/h</string>
|
||||
<string name="liter_per_minute">Litre/minute</string>
|
||||
<string name="liter_per_minute_short">L/m</string>
|
||||
<string name="liter_per_second">Litre/second</string>
|
||||
<string name="liter_per_second_short">L/s</string>
|
||||
<string name="milliliter_per_hour">Millilitre/hour</string>
|
||||
<string name="milliliter_per_hour_short">mL/h</string>
|
||||
<string name="milliliter_per_minute">Millilitre/minute</string>
|
||||
<string name="milliliter_per_minute_short">mL/m</string>
|
||||
<string name="milliliter_per_second">Millilitre/second</string>
|
||||
<string name="milliliter_per_second_short">mL/s</string>
|
||||
<string name="cubic_meter_per_hour">Cubic Metre/hour</string>
|
||||
<string name="cubic_meter_per_hour_short">m3/h</string>
|
||||
<string name="cubic_meter_per_minute">Cubic Metre/minute</string>
|
||||
<string name="cubic_meter_per_minute_short">m3/m</string>
|
||||
<string name="cubic_meter_per_second">Cubic Metre/second</string>
|
||||
<string name="cubic_meter_per_second_short">m3/s</string>
|
||||
<string name="cubic_millimeter_per_hour">Cubic Millimetre/hour</string>
|
||||
<string name="cubic_millimeter_per_hour_short">mm3/h</string>
|
||||
<string name="cubic_millimeter_per_minute">Cubic Millimetre/minute</string>
|
||||
<string name="cubic_millimeter_per_minute_short">mm3/m</string>
|
||||
<string name="cubic_millimeter_per_second">Cubic Millimetre/second</string>
|
||||
<string name="cubic_millimeter_per_second_short">mm3/s</string>
|
||||
<string name="cubic_foot_per_hour">Cubic Foot/hour</string>
|
||||
<string name="cubic_foot_per_hour_short">ft3/h</string>
|
||||
<string name="cubic_foot_per_minute">Cubic Foot/minute</string>
|
||||
<string name="cubic_foot_per_minute_short">ft3/m</string>
|
||||
<string name="cubic_foot_per_second">Cubic Foot/second</string>
|
||||
<string name="cubic_foot_per_second_short">ft3/s</string>
|
||||
<string name="gallon_per_hour_us">Gallon/hour (U.S.)</string>
|
||||
<string name="gallon_per_hour_us_short">gal/h</string>
|
||||
<string name="gallon_per_minute_us">Gallon/minute (U.S.)</string>
|
||||
<string name="gallon_per_minute_us_short">gal/m</string>
|
||||
<string name="gallon_per_second_us">Gallon/second (U.S.)</string>
|
||||
<string name="gallon_per_second_us_short">gal/s</string>
|
||||
<string name="gallon_per_hour_imperial">Gallon/hour (Imperial)</string>
|
||||
<string name="gallon_per_hour_imperial_short">gal/h</string>
|
||||
<string name="gallon_per_minute_imperial">Gallon/minute (Imperial)</string>
|
||||
<string name="gallon_per_minute_imperial_short">gal/m</string>
|
||||
<string name="gallon_per_second_imperial">Gallon/second (Imperial)</string>
|
||||
<string name="gallon_per_second_imperial_short">gal/s</string>
|
||||
|
||||
<!-- Luminance -->
|
||||
<string name="candela_per_square_meter">Candela/square metre</string>
|
||||
<string name="candela_per_square_meter_short">cd/m^2</string>
|
||||
<string name="candela_per_square_centimeter">Candela/square centimetre</string>
|
||||
<string name="candela_per_square_centimeter_short">cd/cm^2</string>
|
||||
<string name="candela_per_square_foot">Candela/square foot</string>
|
||||
<string name="candela_per_square_foot_short">cd/ft^2</string>
|
||||
<string name="candela_per_square_inch">Candela/square inch</string>
|
||||
<string name="candela_per_square_inch_short">cd/in^2</string>
|
||||
<string name="kilocandela_per_square_meter">Kilocandela/square metre</string>
|
||||
<string name="kilocandela_per_square_meter_short">kcd</string>
|
||||
<string name="stilb">Stilb</string>
|
||||
<string name="stilb_short">sb</string>
|
||||
<string name="lumen_per_square_meter_per_steradian">Lumen/square metre/steradian</string>
|
||||
<string name="lumen_per_square_meter_per_steradian_short">lm/m^2/sr</string>
|
||||
<string name="lumen_per_square_centimeter_per_steradian">Lumen/square centimetre/steradian</string>
|
||||
<string name="lumen_per_square_centimeter_per_steradian_short">lm/cm^2/sr</string>
|
||||
<string name="lumen_per_square_foot_per_steradian">Lumen/square foot/steradian</string>
|
||||
<string name="lumen_per_square_foot_per_steradian_short">lm/ft^2/sr</string>
|
||||
<string name="watt_per_square_centimeter_per_steradian">Watt/square centimetre/steradian</string>
|
||||
<string name="watt_per_square_centimeter_per_steradian_short">W/cm^2/sr</string>
|
||||
<string name="nit">Nit</string>
|
||||
<string name="nit_short">nt</string>
|
||||
<string name="millinit">Millinit</string>
|
||||
<string name="millinit_short">mnt</string>
|
||||
<string name="lambert">Lambert</string>
|
||||
<string name="lambert_short">L</string>
|
||||
<string name="millilambert">Millilambert</string>
|
||||
<string name="millilambert_short">mL</string>
|
||||
<string name="foot_lambert">Foot-lambert</string>
|
||||
<string name="foot_lambert_short">fL</string>
|
||||
<string name="apostilb">Apostilb</string>
|
||||
<string name="apostilb_short">asb</string>
|
||||
<string name="blondel">Blondel</string>
|
||||
<string name="blondel_short">blondel</string>
|
||||
<string name="bril">Bril</string>
|
||||
<string name="bril_short">bril</string>
|
||||
<string name="skot">Skot</string>
|
||||
<string name="skot_short">sk</string>
|
||||
<string name="electrostatic_capacitance">Capacitance</string>
|
||||
<string name="prefix">Prefix</string>
|
||||
<string name="force">Force</string>
|
||||
<string name="torque">Torque</string>
|
||||
<string name="flow_rate">Flow</string>
|
||||
<string name="luminance">Luminance</string>
|
||||
<string name="format_time">Format time</string>
|
||||
<string name="format_time_support">Example: Show 130 minutes as 2h 10m</string>
|
||||
<string name="units_sorting">Units list sorting</string>
|
||||
<string name="units_sorting_support">Change units order</string>
|
||||
|
||||
<!-- Units list sorting -->
|
||||
<string name="sort_by_usage">Usage</string>
|
||||
<string name="sort_by_alphabetical">Alphabetical</string>
|
||||
<string name="sort_by_scale_desc">Scale (Desc.)</string>
|
||||
<string name="sort_by_scale_asc">Scale (Asc.)</string>
|
||||
<string name="color_theme_support">Pick a theming mode</string>
|
||||
<string name="color_scheme">Colour scheme</string>
|
||||
<string name="selected_color">Selected colour</string>
|
||||
<string name="monet_mode">Selected style</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
|
||||
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
|
||||
|
||||
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
|
||||
<string name="formatting_setting_support">Precision and numbers appearance</string>
|
||||
<string name="divide_by_zero_error">Can\'t divide by 0</string>
|
||||
<string name="date_difference">Date difference</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
|
||||
<string name="select_time">Select time</string>
|
||||
<string name="date_difference_start">Start</string>
|
||||
<string name="date_difference_end">End</string>
|
||||
<string name="date_difference_result">Difference</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_years">Years</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_months">Months</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_days">Days</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_hours">Hours</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_minutes">Minutes</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
|
||||
|
||||
Used in this dialog window. Should be short -->
|
||||
<string name="next_label">Next</string>
|
||||
<string name="formatting_setting_preview_box_label">Preview (click to switch)</string>
|
||||
</resources>
|
@ -659,7 +659,9 @@
|
||||
<string name="theme_setting">Thèmes</string>
|
||||
<string name="precision_setting">Précision</string>
|
||||
<string name="separator_setting">Séparateur</string>
|
||||
<string name="output_format_setting">Format de sortie</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="exponential_notation_setting">Notation exponentielle</string>
|
||||
<string name="unit_groups_setting">Groupes d\'unités</string>
|
||||
<string name="currency_rates_note_title">Note</string>
|
||||
<string name="currency_rates_note_text">Les taux de change sont mis à jour quotidiennement. L\'application ne permet pas de suivre le marché en temps réel.</string>
|
||||
@ -667,21 +669,22 @@
|
||||
<string name="privacy_policy">Politique de confidentialité</string>
|
||||
<string name="third_party_licenses">Licences tierces</string>
|
||||
<string name="rate_this_app">Évaluer l\'application</string>
|
||||
<string name="formatting_settings_group">Formattage</string>
|
||||
<string name="formatting_setting">Formattage</string>
|
||||
<string name="additional_settings_group">Additional</string>
|
||||
|
||||
<!-- Precision -->
|
||||
<string name="precision_setting_support">Nombre de décimales</string>
|
||||
<string name="precision_setting_info">Les valeurs converties peuvent avoir une précision supérieure à la précision préférée.</string>
|
||||
<string name="max_precision">1 000 (Max)</string>
|
||||
<string name="max_precision">%1$s (Max)</string>
|
||||
|
||||
<!-- Separator -->
|
||||
<string name="separator_setting_support">Symbole de séparation de groupe</string>
|
||||
<string name="period">Période (42.069,12)</string>
|
||||
<string name="comma">Virgule (42,069.12)</string>
|
||||
<string name="spaces">Espaces (42 069.12)</string>
|
||||
<string name="plain">Défaut</string>
|
||||
<string name="force_auto_mode">Auto</string>
|
||||
<string name="period">Période</string>
|
||||
<string name="comma">Virgule</string>
|
||||
|
||||
<!-- Fuzzy -->
|
||||
<string name="space">Espaces</string>
|
||||
<string name="auto_label">Auto</string>
|
||||
<string name="force_light_mode">Clair</string>
|
||||
<string name="force_dark_mode">Sombre</string>
|
||||
<string name="force_amoled_mode">AMOLED Noir</string>
|
||||
|
1101
core/base/src/main/res/values-it/strings.xml
Normal file
@ -659,7 +659,7 @@
|
||||
<string name="theme_setting">Темы</string>
|
||||
<string name="precision_setting">Точность</string>
|
||||
<string name="separator_setting">Разделитель</string>
|
||||
<string name="output_format_setting">Формат вывода</string>
|
||||
<string name="exponential_notation_setting">Экспоненциальная нотация</string>
|
||||
<string name="unit_groups_setting">Группы величин</string>
|
||||
<string name="currency_rates_note_setting">Неправильные курсы валют?</string>
|
||||
<string name="currency_rates_note_title">Внимание</string>
|
||||
@ -668,35 +668,31 @@
|
||||
<string name="privacy_policy">Политика конфиденциальности</string>
|
||||
<string name="third_party_licenses">Лицензии третьих лиц</string>
|
||||
<string name="rate_this_app">Оценить приложение</string>
|
||||
<string name="formatting_settings_group">Форматирование</string>
|
||||
<string name="formatting_setting">Форматирование</string>
|
||||
<string name="additional_settings_group">Дополнительное</string>
|
||||
|
||||
<!-- Precision -->
|
||||
<string name="precision_setting_support">Количество десятичных знаков</string>
|
||||
<string name="precision_setting_info">Переводимые значения могут иметь точность выше предпочтительной.</string>
|
||||
<string name="max_precision">1 000 (Максимум)</string>
|
||||
<string name="max_precision">%1$s (Максимум)</string>
|
||||
|
||||
<!-- Separator -->
|
||||
<string name="separator_setting_support">Символ разделителя</string>
|
||||
<string name="period">Точка (42.069,12)</string>
|
||||
<string name="comma">Запятая (42,069.12)</string>
|
||||
<string name="spaces">Пробел (42 069.12)</string>
|
||||
<string name="period">Точка</string>
|
||||
<string name="comma">Запятая</string>
|
||||
<string name="space">Пробел</string>
|
||||
|
||||
<!-- Output format -->
|
||||
<string name="output_format_setting_support">Формат результата перевода</string>
|
||||
<string name="output_format_setting_info">Инженерный формат выглядит как 1E-21</string>
|
||||
<string name="plain">По умолчанию</string>
|
||||
<string name="allow_engineering">Разрешить инженерный</string>
|
||||
<string name="force_engineering">Преимущественно инженерный</string>
|
||||
<string name="exponential_notation_setting_support">Замените часть числа на E</string>
|
||||
|
||||
<!-- Theme -->
|
||||
<string name="theme_setting_support">Внешний вид приложения</string>
|
||||
<string name="force_auto_mode">Автоматическая</string>
|
||||
<string name="auto_label">Авто</string>
|
||||
<string name="force_light_mode">Светлая</string>
|
||||
<string name="force_dark_mode">Темная</string>
|
||||
<string name="force_dark_mode">Тёмная</string>
|
||||
<string name="color_theme">Цветовая тема</string>
|
||||
<string name="force_amoled_mode">Темная AMOLED</string>
|
||||
<string name="force_amoled_mode_support">Использовать черный фон в темных темах</string>
|
||||
<string name="force_amoled_mode">Чёрная AMOLED</string>
|
||||
<string name="force_amoled_mode_support">Использовать чёрный фон в тёмной теме</string>
|
||||
<string name="enable_dynamic_colors">Динамичные цвета</string>
|
||||
<string name="enable_dynamic_colors_support">Использовать цвета обоев</string>
|
||||
|
||||
@ -1048,7 +1044,7 @@
|
||||
<string name="skot">Скот</string>
|
||||
<string name="skot_short">ск</string>
|
||||
<string name="electrostatic_capacitance">Емкость</string>
|
||||
<string name="prefix">Префиск</string>
|
||||
<string name="prefix">Префикс</string>
|
||||
<string name="force">Сила</string>
|
||||
<string name="torque">Момент</string>
|
||||
<string name="flow_rate">Течение</string>
|
||||
@ -1067,4 +1063,39 @@
|
||||
<string name="color_scheme">Цветовая схема</string>
|
||||
<string name="selected_color">Выбранный цвет</string>
|
||||
<string name="monet_mode">Выбранный стиль</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/Gj9ZCNPXIfKddDQgccvboVFz.jpg
|
||||
https://s3.eu-west-1.amazonaws.com/po-pub/i/prtM85P6x1fMuLg1I0zbkceo.png
|
||||
|
||||
Maybe this can be labeled better? Let me know. It should be something that can describe content of the Formatting screen. -->
|
||||
<string name="formatting_setting_support">Точность и представление чисел</string>
|
||||
<string name="divide_by_zero_error">Нельзя делить на 0</string>
|
||||
<string name="date_difference">Разница между датами</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/uWOHJmIq9riqsq7PO82ZQp3a.png -->
|
||||
<string name="select_time">Выберите время</string>
|
||||
<string name="date_difference_start">Начало</string>
|
||||
<string name="date_difference_end">Конец</string>
|
||||
<string name="date_difference_result">Разница</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_years">Лет</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_months">Месяцев</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_days">Дней</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_hours">Часов</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_difference_minutes">Минут</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
|
||||
|
||||
Used in this dialog window. Should be short -->
|
||||
<string name="next_label">Далее</string>
|
||||
<string name="formatting_setting_preview_box_label">Предпросмотр (нажмите для переключения)</string>
|
||||
</resources>
|
@ -16,6 +16,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@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)
|
||||
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> = 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<String> =
|
||||
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
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<List<Placeable>>()
|
||||
val crossAxisSizes = mutableListOf<Int>()
|
||||
val crossAxisPositions = mutableListOf<Int>()
|
||||
|
||||
var mainAxisSpace = 0
|
||||
var crossAxisSpace = 0
|
||||
|
||||
val currentSequence = mutableListOf<Placeable>()
|
||||
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 }
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Dp>,
|
||||
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
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<UnittoDrawerState>,
|
||||
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<UnittoDrawerState> {
|
||||
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<UnittoDrawerState>.isOpen
|
||||
get() = this.currentValue == UnittoDrawerState.OPEN
|
||||
|
||||
suspend fun AnchoredDraggableState<UnittoDrawerState>.close() {
|
||||
this.animateTo(UnittoDrawerState.CLOSED)
|
||||
}
|
||||
|
||||
suspend fun AnchoredDraggableState<UnittoDrawerState>.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Float>,
|
||||
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 }
|
||||
)
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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>): 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
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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))
|
||||
)
|
||||
}
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -59,6 +59,7 @@ class CalculatorHistoryRepository @Inject constructor(
|
||||
private fun List<CalculatorHistoryEntity>.toHistoryItemList(): List<HistoryItem> {
|
||||
return this.map {
|
||||
HistoryItem(
|
||||
id = it.entityId,
|
||||
date = Date(it.timestamp),
|
||||
expression = it.expression,
|
||||
result = it.result
|
||||
|
@ -26,4 +26,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(mapOf("path" to ":core:base")))
|
||||
testImplementation(libs.junit)
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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())
|
||||
}
|
@ -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 {
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> {
|
||||
var cursor = 0
|
||||
val tokens: MutableList<String> = 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<String>.repairLexicon(): List<String> {
|
||||
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<String>.missingClosingBrackets(): List<String> {
|
||||
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<String>.missingMultiply(): List<String> {
|
||||
val results = this.toMutableList()
|
||||
val insertIndexes = mutableListOf<Int>()
|
||||
|
||||
// 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<String>.unpackAlPercents(): List<String> {
|
||||
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<String>.unpackPercentAt(percentIndex: Int): List<String> {
|
||||
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<String>.getNumberOrExpressionBefore(pos: Int): List<String> {
|
||||
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<String>.getBaseBefore(pos: Int): List<String> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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")
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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, "...")
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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")
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.%")
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <T : Throwable?> assertExprFail(
|
||||
expectedThrowable: Class<T>?,
|
||||
expr: String,
|
||||
radianMode: Boolean = true
|
||||
) {
|
||||
assertThrows(expectedThrowable) {
|
||||
Expression(expr, radianMode = radianMode).calculate()
|
||||
}
|
||||
}
|
||||
|
||||
fun assertLex(expected: List<String>, actual: String) =
|
||||
assertEquals(expected, Tokenizer(actual).tokenize())
|
||||
|
||||
fun assertLex(expected: String, actual: String) =
|
||||
assertEquals(expected, Tokenizer(actual).tokenize().joinToString(""))
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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")
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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<UnitGroup> by lazy {
|
||||
UnitGroup.values().toList()
|
||||
|
@ -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 *;
|
||||
|
@ -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<String, BigDecimal>
|
||||
) {
|
||||
suspend fun updateBasicUnitsForCurrencies(
|
||||
unitFrom: AbstractUnit
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val conversions: Map<String, BigDecimal> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|
@ -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<AbstractUnit> by lazy {
|
||||
|