From 0bf3ac615517a9272f772ccd355d80cb2b86f497 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Sun, 29 Jan 2023 12:13:07 +0400 Subject: [PATCH] Tools screen Added basic tools screen Added epoch converter screen Little refactor to avoid code duplication --- app/build.gradle.kts | 2 + .../com/sadellie/unitto/UnittoNavigation.kt | 14 ++ .../main/java/UnittoLibraryFeaturePlugin.kt | 5 + core/base/src/main/res/values/strings.xml | 9 + .../unitto/core/ui/common}/KeyboardButton.kt | 7 +- .../unitto/core/ui/common/UnittoTopAppBar.kt | 63 +++++++ .../unitto/feature/converter/MainScreen.kt | 12 +- .../feature/converter/components/Keyboard.kt | 1 + .../navigation/ConverterNavigation.kt | 2 + feature/epoch/.gitignore | 1 + feature/epoch/build.gradle.kts | 28 +++ feature/epoch/consumer-rules.pro | 0 feature/epoch/src/main/AndroidManifest.xml | 4 + .../unitto/feature/epoch/EpochScreen.kt | 145 ++++++++++++++++ .../unitto/feature/epoch/EpochViewModel.kt | 132 ++++++++++++++ .../feature/epoch/component/DateTextField.kt | 162 ++++++++++++++++++ .../feature/epoch/component/EpochKeyboard.kt | 87 ++++++++++ .../unitto/feature/epoch/component/TopPart.kt | 46 +++++ .../feature/epoch/component/UnixTextField.kt | 51 ++++++ .../epoch/navigation/EpochNavigation.kt | 40 +++++ feature/tools/.gitignore | 1 + feature/tools/build.gradle.kts | 28 +++ feature/tools/consumer-rules.pro | 0 feature/tools/src/main/AndroidManifest.xml | 4 + .../unitto/feature/tools/ToolsScreen.kt | 50 ++++++ .../tools/navigation/ToolsNavigation.kt | 42 +++++ settings.gradle.kts | 2 + 27 files changed, 933 insertions(+), 5 deletions(-) rename {feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components => core/ui/src/main/java/com/sadellie/unitto/core/ui/common}/KeyboardButton.kt (95%) create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoTopAppBar.kt create mode 100644 feature/epoch/.gitignore create mode 100644 feature/epoch/build.gradle.kts create mode 100644 feature/epoch/consumer-rules.pro create mode 100644 feature/epoch/src/main/AndroidManifest.xml create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochViewModel.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/DateTextField.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/EpochKeyboard.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/TopPart.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/UnixTextField.kt create mode 100644 feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/navigation/EpochNavigation.kt create mode 100644 feature/tools/.gitignore create mode 100644 feature/tools/build.gradle.kts create mode 100644 feature/tools/consumer-rules.pro create mode 100644 feature/tools/src/main/AndroidManifest.xml create mode 100644 feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt create mode 100644 feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5decedba..2fc187eb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,6 +110,8 @@ dependencies { implementation(project(mapOf("path" to ":feature:converter"))) implementation(project(mapOf("path" to ":feature:settings"))) implementation(project(mapOf("path" to ":feature:unitslist"))) + implementation(project(mapOf("path" to ":feature:tools"))) + implementation(project(mapOf("path" to ":feature:epoch"))) implementation(project(mapOf("path" to ":data"))) implementation(project(mapOf("path" to ":core:base"))) implementation(project(mapOf("path" to ":core:ui"))) diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt index 8c2c73c4..30633b17 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt @@ -24,10 +24,14 @@ import androidx.navigation.compose.NavHost import com.sadellie.unitto.feature.converter.MainViewModel import com.sadellie.unitto.feature.converter.navigation.converterRoute import com.sadellie.unitto.feature.converter.navigation.converterScreen +import com.sadellie.unitto.feature.epoch.navigation.epochScreen +import com.sadellie.unitto.feature.epoch.navigation.navigateToEpoch import com.sadellie.unitto.feature.settings.SettingsViewModel import com.sadellie.unitto.feature.settings.navigation.navigateToSettings import com.sadellie.unitto.feature.settings.navigation.navigateToUnitGroups import com.sadellie.unitto.feature.settings.navigation.settingGraph +import com.sadellie.unitto.feature.tools.navigation.navigateToTools +import com.sadellie.unitto.feature.tools.navigation.toolsScreen import com.sadellie.unitto.feature.unitslist.SecondViewModel import com.sadellie.unitto.feature.unitslist.navigation.leftScreen import com.sadellie.unitto.feature.unitslist.navigation.navigateToLeftSide @@ -53,6 +57,7 @@ fun UnittoNavigation( navController.navigateToRightSide(unitFrom, unitTo, input) }, navigateToSettings = { navController.navigateToSettings() }, + navigateToTools = { navController.navigateToTools() }, viewModel = mainViewModel ) @@ -75,5 +80,14 @@ fun UnittoNavigation( themmoController = themmoController, navController = navController ) + + toolsScreen( + navigateUpAction = navController::navigateUp, + navigateToEpoch = navController::navigateToEpoch + ) + + epochScreen( + navigateUpAction = navController::navigateUp + ) } } diff --git a/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt b/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt index ab7404da..dbcd8efd 100644 --- a/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt +++ b/build-logic/convention/src/main/java/UnittoLibraryFeaturePlugin.kt @@ -18,12 +18,17 @@ import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType class UnittoLibraryFeaturePlugin : Plugin { override fun apply(target: Project) { with(target) { + val libs = extensions.getByType().named("libs") dependencies { + "implementation"(libs.findLibrary("androidx.navigation").get()) + "implementation"(project(mapOf("path" to ":data"))) "implementation"(project(mapOf("path" to ":core:base"))) "implementation"(project(mapOf("path" to ":core:ui"))) diff --git a/core/base/src/main/res/values/strings.xml b/core/base/src/main/res/values/strings.xml index 76656d81..0f76ff0a 100644 --- a/core/base/src/main/res/values/strings.xml +++ b/core/base/src/main/res/values/strings.xml @@ -992,6 +992,7 @@ Convert from Convert to Settings + Tools Themes @@ -1014,6 +1015,10 @@ Formatting Additional + + Epoch converter + Convert Unix timestamp + Number of decimal places Converted values may have a precision higher than the preferred one. @@ -1092,4 +1097,8 @@ Learn about the app Disable and rearrange units + + y + m + \ No newline at end of file diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt similarity index 95% rename from feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/KeyboardButton.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt index 03458c09..801eebc9 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/KeyboardButton.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt @@ -1,6 +1,6 @@ /* * Unitto is a unit converter for Android - * Copyright (c) 2022-2022 Elshan Agaev + * 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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.converter.components +package com.sadellie.unitto.core.ui.common import android.view.HapticFeedbackConstants import androidx.compose.animation.core.FastOutSlowInEasing @@ -36,7 +36,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp -import com.sadellie.unitto.core.ui.common.UnittoButton import com.sadellie.unitto.core.ui.theme.NumbersTextStyleTitleLarge /** @@ -51,7 +50,7 @@ import com.sadellie.unitto.core.ui.theme.NumbersTextStyleTitleLarge * @param onClick Action to perform when clicking this button. */ @Composable -internal fun KeyboardButton( +fun KeyboardButton( modifier: Modifier = Modifier, digit: String, allowVibration: Boolean, diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoTopAppBar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoTopAppBar.kt new file mode 100644 index 00000000..5b135c05 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoTopAppBar.kt @@ -0,0 +1,63 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2022 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.core.ui.common + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +/** + * Commonly used TopAppBar with scroll behavior. + * + * @param title Text that is displayed in top bar. + * @param navigateUpAction Action when user click arrow button at the top. + * @param content Content that can be scrolled. Don't forget to use padding values. + */ +@Composable +fun UnittoTopAppBar( + title: String, + navigateUpAction: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + Scaffold( + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { + Text(text = title) + }, + navigationIcon = { + NavigateUpButton { navigateUpAction() } + }, + scrollBehavior = scrollBehavior + ) + }, + content = content + ) +} diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/MainScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/MainScreen.kt index fab4c833..c52e59e5 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/MainScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/MainScreen.kt @@ -21,6 +21,7 @@ package com.sadellie.unitto.feature.converter import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.Science import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -37,8 +38,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.sadellie.unitto.core.ui.common.AnimatedTopBarText import com.sadellie.unitto.core.ui.R +import com.sadellie.unitto.core.ui.common.AnimatedTopBarText import com.sadellie.unitto.feature.converter.components.Keyboard import com.sadellie.unitto.feature.converter.components.PortraitLandscape import com.sadellie.unitto.feature.converter.components.TopScreenPart @@ -49,6 +50,7 @@ internal fun MainScreen( navigateToLeftScreen: (String) -> Unit, navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit, navigateToSettings: () -> Unit, + navigateToTools: () -> Unit, viewModel: MainViewModel = viewModel() ) { var launched: Boolean by rememberSaveable { mutableStateOf(false) } @@ -61,6 +63,14 @@ internal fun MainScreen( CenterAlignedTopAppBar( modifier = Modifier, title = { AnimatedTopBarText(launched) }, + navigationIcon = { + IconButton(onClick = navigateToTools) { + Icon( + Icons.Outlined.Science, + contentDescription = stringResource(R.string.open_settings_description) + ) + } + }, actions = { IconButton(onClick = navigateToSettings) { Icon( diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt index c463f530..8faf909c 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt @@ -56,6 +56,7 @@ import com.sadellie.unitto.core.base.KEY_PLUS import com.sadellie.unitto.core.base.KEY_RIGHT_BRACKET import com.sadellie.unitto.core.base.KEY_SQRT import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.KeyboardButton import com.sadellie.unitto.feature.converter.ConverterMode /** diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt index 9a1feecd..d5081944 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/navigation/ConverterNavigation.kt @@ -29,6 +29,7 @@ fun NavGraphBuilder.converterScreen( navigateToLeftScreen: (String) -> Unit, navigateToRightScreen: (unitFrom: String, unitTo: String, input: String) -> Unit, navigateToSettings: () -> Unit, + navigateToTools: () -> Unit, viewModel: MainViewModel ) { composable(converterRoute) { @@ -36,6 +37,7 @@ fun NavGraphBuilder.converterScreen( navigateToLeftScreen = navigateToLeftScreen, navigateToRightScreen = navigateToRightScreen, navigateToSettings = navigateToSettings, + navigateToTools = navigateToTools, viewModel = viewModel, ) } diff --git a/feature/epoch/.gitignore b/feature/epoch/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/epoch/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/epoch/build.gradle.kts b/feature/epoch/build.gradle.kts new file mode 100644 index 00000000..3a07fd1f --- /dev/null +++ b/feature/epoch/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +plugins { + id("unitto.library") + id("unitto.library.compose") + id("unitto.library.feature") + id("unitto.android.hilt") +} + +android { + namespace = "com.sadellie.unitto.feature.epoch" +} diff --git a/feature/epoch/consumer-rules.pro b/feature/epoch/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/epoch/src/main/AndroidManifest.xml b/feature/epoch/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/epoch/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt new file mode 100644 index 00000000..93cae445 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochScreen.kt @@ -0,0 +1,145 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sadellie.unitto.core.ui.common.UnittoTopAppBar +import com.sadellie.unitto.feature.epoch.component.DateTextField +import com.sadellie.unitto.feature.epoch.component.EpochKeyboard +import com.sadellie.unitto.feature.epoch.component.TopPart +import com.sadellie.unitto.feature.epoch.component.UnixTextField + +@Composable +internal fun EpochRoute( + navigateUpAction: () -> Unit, + viewModel: EpochViewModel = hiltViewModel() +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + EpochScreen( + navigateUpAction = navigateUpAction, + uiState = uiState.value, + addSymbol = viewModel::addSymbol, + deleteSymbol = viewModel::deleteSymbol, + swap = viewModel::swap, + clearSymbols = viewModel::clearSymbols + ) +} + +@Composable +private fun EpochScreen( + navigateUpAction: () -> Unit, + uiState: EpochUIState, + addSymbol: (String) -> Unit, + deleteSymbol: () -> Unit, + clearSymbols: () -> Unit, + swap: () -> Unit +) { + UnittoTopAppBar( + title = stringResource(R.string.epoch_converter), + navigateUpAction = navigateUpAction + ) { padding -> + Column( + modifier = Modifier.padding(padding), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TopPart( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + .weight(1f), + unixToDate = !uiState.dateToUnix, + dateField = { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .animateItemPlacement() + ) { + DateTextField( + modifier = Modifier.fillMaxWidth(), + date = uiState.dateField + ) + Text( + text = "date", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + } + }, + unixField = { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .animateItemPlacement() + ) { + UnixTextField( + modifier = Modifier.fillMaxWidth(), + unix = uiState.unixField + ) + Text( + text = "unix", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + } + } + ) + + Button( + onClick = swap, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + Icon(Icons.Default.SwapHoriz, null) + Spacer(modifier = Modifier.width(8.dp)) + Text("SWAP") + } + + EpochKeyboard( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + .weight(1f), + addSymbol = addSymbol, + clearSymbols = clearSymbols, + deleteSymbol = deleteSymbol + ) + } + } +} diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochViewModel.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochViewModel.kt new file mode 100644 index 00000000..ba141f51 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/EpochViewModel.kt @@ -0,0 +1,132 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import java.math.BigDecimal +import java.util.* +import javax.inject.Inject + +data class EpochUIState( + val dateField: String = "", + val unixField: String = "", + val dateToUnix: Boolean = true +) + +@HiltViewModel +class EpochViewModel @Inject constructor() : ViewModel() { + + private val _input: MutableStateFlow = MutableStateFlow("") + private val _fromDateToUnix: MutableStateFlow = MutableStateFlow(true) + + val uiState: StateFlow = combine( + _input, _fromDateToUnix + ) { input, fromDateToUnix -> + if (fromDateToUnix) { + EpochUIState( + dateField = input, + unixField = convertDateToUnix(input), + dateToUnix = fromDateToUnix + ) + } else { + EpochUIState( + unixField = input, + dateField = convertUnixToDate(input), + dateToUnix = fromDateToUnix + ) + } + }.stateIn( + viewModelScope, SharingStarted.WhileSubscribed(5000L), EpochUIState() + ) + + fun addSymbol(symbol: String) { + val maxSymbols = if (_fromDateToUnix.value) 14 else 10 + if (_input.value.length < maxSymbols) _input.update { it + symbol } + } + + fun deleteSymbol() = _input.update { it.dropLast(1) } + + fun clearSymbols() = _input.update { "" } + + fun swap() = _fromDateToUnix.update { + clearSymbols() + !it + } + + private fun convertDateToUnix(date: String): String { + // Here we add some zeros, so that input is 14 symbols long + val inputWithPadding = date.padEnd(14, '0') + + // Now we break input that is 14 symbols into pieces + val day = inputWithPadding.substring(0, 2) + val month = inputWithPadding.substring(2, 4) + val year = inputWithPadding.substring(4, 8) + val hour = inputWithPadding.substring(8, 10) + val minute = inputWithPadding.substring(10, 12) + val second = inputWithPadding.substring(12, 14) + + val cal = Calendar.getInstance() + cal.set( + year.toIntOrNull() ?: 1970, + (month.toIntOrNull() ?: 1) - 1, + day.toIntOrNull() ?: 0, + hour.toIntOrNull() ?: 0, + minute.toIntOrNull() ?: 0, + second.toIntOrNull() ?: 0, + ) + return (cal.timeInMillis / 1000).toString() + } + + private fun convertUnixToDate(unix: String): String { + var date = "" + val cal2 = Calendar.getInstance() + cal2.clear() + cal2.isLenient = true + + // This lets us bypass calendars limits (it uses Int, we want BigDecimal) + try { + val unixBg = BigDecimal(unix) + val division = unixBg.divideAndRemainder(BigDecimal(Int.MAX_VALUE)) + val intTimes = division.component1() + val rem = division.component2() + repeat(intTimes.intValueExact()) { + cal2.add(Calendar.SECOND, Int.MAX_VALUE) + } + cal2.add(Calendar.SECOND, rem.intValueExact()) + } catch (e: NumberFormatException) { + return "" + } + date += cal2.get(Calendar.DAY_OF_MONTH).toString().padStart(2, '0') + date += (cal2.get(Calendar.MONTH) + 1).toString().padStart(2, '0') + // Year is 4 symbols long + date += cal2.get(Calendar.YEAR).toString().padStart(4, '0') + date += cal2.get(Calendar.HOUR_OF_DAY).toString().padStart(2, '0') + date += cal2.get(Calendar.MINUTE).toString().padStart(2, '0') + date += cal2.get(Calendar.SECOND).toString().padStart(2, '0') + return date + } +} diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/DateTextField.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/DateTextField.kt new file mode 100644 index 00000000..88b40733 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/DateTextField.kt @@ -0,0 +1,162 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch.component + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.with +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium +import com.sadellie.unitto.feature.epoch.R + +@Composable +fun DateTextField( + modifier: Modifier, + date: String +) { + val inputWithPadding: String = date.padEnd(14, '0') + + val day: String = inputWithPadding.substring(0, 2) + val month: String = inputWithPadding.substring(2, 4) + val year: String = inputWithPadding.substring(4, 8) + val hour: String = inputWithPadding.substring(8, 10) + val minute: String = inputWithPadding.substring(10, 12) + val second: String = inputWithPadding.substring(12, 14) + + fun inFocus(range: Int): Boolean = date.length > range + + Row( + modifier = modifier + .height(IntrinsicSize.Min) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.End) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + Row { + DateTextAnimated(text = day, focus = inFocus(0)) + DateText(text = stringResource(R.string.day_short), focus = inFocus(0)) + } + Row { + DateTextAnimated(text = month, focus = inFocus(2)) + DateText(text = stringResource(R.string.month_short), focus = inFocus(2)) + } + Row { + DateTextAnimated(text = year, focus = inFocus(4)) + DateText(text = stringResource(R.string.year_short), focus = inFocus(4)) + } + } + + Divider( + modifier = Modifier + .fillMaxHeight() + .width(1.dp) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + DateTextAnimated(text = hour, focus = inFocus(8)) + DateText(text = ":", focus = inFocus(8)) + DateTextAnimated(text = minute, focus = inFocus(10)) + DateText(text = ":", focus = inFocus(10)) + DateTextAnimated(text = second, focus = inFocus(12)) + } + } +} + +@Composable +private fun BaseDateText( + focus: Boolean, + content: @Composable (Color) -> Unit +) { + val color = animateColorAsState( + if (focus) { + MaterialTheme.colorScheme.onBackground + } else { + MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) + } + ) + content(color.value) +} + +@Composable +private fun DateText( + focus: Boolean = true, + text: String +) { + BaseDateText(focus = focus) { color -> + Text( + text = text, + style = NumbersTextStyleDisplayMedium, + color = color + ) + } +} + +@Composable +private fun DateTextAnimated( + focus: Boolean = true, + text: String +) { + BaseDateText(focus = focus) { color -> + AnimatedContent( + targetState = text, + transitionSpec = { + if (targetState.toInt() > initialState.toInt()) { + slideInVertically { height -> height } + fadeIn() with + slideOutVertically { height -> -height } + fadeOut() + } else { + slideInVertically { height -> -height } + fadeIn() with + slideOutVertically { height -> height } + fadeOut() + }.using( + SizeTransform(clip = false) + ) + } + ) { + Text( + text = it, + style = NumbersTextStyleDisplayMedium, + color = color + ) + } + } +} diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/EpochKeyboard.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/EpochKeyboard.kt new file mode 100644 index 00000000..47730f7f --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/EpochKeyboard.kt @@ -0,0 +1,87 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.base.KEY_0 +import com.sadellie.unitto.core.base.KEY_1 +import com.sadellie.unitto.core.base.KEY_2 +import com.sadellie.unitto.core.base.KEY_3 +import com.sadellie.unitto.core.base.KEY_4 +import com.sadellie.unitto.core.base.KEY_5 +import com.sadellie.unitto.core.base.KEY_6 +import com.sadellie.unitto.core.base.KEY_7 +import com.sadellie.unitto.core.base.KEY_8 +import com.sadellie.unitto.core.base.KEY_9 +import com.sadellie.unitto.core.base.KEY_CLEAR +import com.sadellie.unitto.core.ui.common.KeyboardButton + +@Composable +internal fun EpochKeyboard( + modifier: Modifier, + addSymbol: (String) -> Unit, + clearSymbols: () -> Unit, + deleteSymbol: () -> Unit +) { + Column( + modifier = modifier + ) { + // Button modifier + val bModifier = Modifier + .fillMaxSize() + .weight(1f) + .padding(4.dp) + // Column modifier + val cModifier = Modifier.weight(1f) + val dModifier = Modifier + .fillMaxSize() + .weight(2f) + .padding(4.dp) + Row(cModifier) { + KeyboardButton(bModifier, KEY_7, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_8, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_9, allowVibration = false, onClick = addSymbol) + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_4, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_5, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_6, allowVibration = false, onClick = addSymbol) + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_1, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_2, allowVibration = false, onClick = addSymbol) + KeyboardButton(bModifier, KEY_3, allowVibration = false, onClick = addSymbol) + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_0, allowVibration = false, onClick = addSymbol) + KeyboardButton( + dModifier, + KEY_CLEAR, + allowVibration = false, + onLongClick = clearSymbols + ) { deleteSymbol() } + } + } +} \ No newline at end of file diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/TopPart.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/TopPart.kt new file mode 100644 index 00000000..64976534 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/TopPart.kt @@ -0,0 +1,46 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun TopPart( + modifier: Modifier, + unixToDate: Boolean, + dateField: @Composable() (LazyItemScope.() -> Unit), + unixField: @Composable() (LazyItemScope.() -> Unit), +) { + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.Bottom + ) { + if (unixToDate) { + item("unix") { unixField() } + item("date") { dateField() } + } else { + item("date") { dateField() } + item("unix") { unixField() } + } + } +} diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/UnixTextField.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/UnixTextField.kt new file mode 100644 index 00000000..36898644 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/component/UnixTextField.kt @@ -0,0 +1,51 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch.component + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium + +@Composable +fun UnixTextField( + modifier: Modifier, + unix: String +) { + Row( + modifier = modifier + .height(IntrinsicSize.Min) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.End + ) { + Text( + text = unix.ifEmpty { "0" }, + modifier = modifier, + style = NumbersTextStyleDisplayMedium, + textAlign = TextAlign.End + ) + } +} diff --git a/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/navigation/EpochNavigation.kt b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/navigation/EpochNavigation.kt new file mode 100644 index 00000000..dde15761 --- /dev/null +++ b/feature/epoch/src/main/java/com/sadellie/unitto/feature/epoch/navigation/EpochNavigation.kt @@ -0,0 +1,40 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.epoch.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.sadellie.unitto.feature.epoch.EpochRoute + +private const val epochRoute = "epoch_route" + +fun NavController.navigateToEpoch() { + navigate(epochRoute) +} + +fun NavGraphBuilder.epochScreen( + navigateUpAction: () -> Unit +) { + composable(epochRoute) { + EpochRoute( + navigateUpAction = navigateUpAction + ) + } +} diff --git a/feature/tools/.gitignore b/feature/tools/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/tools/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/tools/build.gradle.kts b/feature/tools/build.gradle.kts new file mode 100644 index 00000000..2b933c23 --- /dev/null +++ b/feature/tools/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +plugins { + id("unitto.library") + id("unitto.library.compose") + id("unitto.library.feature") + id("unitto.android.hilt") +} + +android { + namespace = "com.sadellie.unitto.feature.tools" +} diff --git a/feature/tools/consumer-rules.pro b/feature/tools/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/tools/src/main/AndroidManifest.xml b/feature/tools/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/tools/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt new file mode 100644 index 00000000..ee29d13a --- /dev/null +++ b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/ToolsScreen.kt @@ -0,0 +1,50 @@ +package com.sadellie.unitto.feature.tools + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.sadellie.unitto.core.ui.common.UnittoLargeTopAppBar + +@Composable +internal fun ToolsScreen( + navigateUpAction: () -> Unit, + navigateToEpoch: () -> Unit +) { + UnittoLargeTopAppBar( + title = stringResource(R.string.tools_screen), + navigateUpAction = navigateUpAction + ) { padding -> + LazyColumn(contentPadding = padding) { + item { + ListItem( + leadingContent = { + Icon( + Icons.Default.Schedule, + null // TODO describe + ) + }, + headlineText = { Text(stringResource(R.string.epoch_converter)) }, + supportingText = { Text(stringResource(R.string.epoch_converter_support)) }, + modifier = Modifier.clickable { navigateToEpoch() } + ) + } + } + } +} + +@Preview +@Composable +internal fun PreviewToolsScreen() { + ToolsScreen( + navigateUpAction = {}, + navigateToEpoch = {} + ) +} \ No newline at end of file diff --git a/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt new file mode 100644 index 00000000..30ff5f15 --- /dev/null +++ b/feature/tools/src/main/java/com/sadellie/unitto/feature/tools/navigation/ToolsNavigation.kt @@ -0,0 +1,42 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.tools.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.sadellie.unitto.feature.tools.ToolsScreen + +private const val toolsRoute = "tools_route" + +fun NavController.navigateToTools() { + navigate(toolsRoute) +} + +fun NavGraphBuilder.toolsScreen( + navigateUpAction: () -> Unit, + navigateToEpoch: () -> Unit +) { + composable(toolsRoute) { + ToolsScreen( + navigateUpAction = navigateUpAction, + navigateToEpoch = navigateToEpoch + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 196d0f91..54780195 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,5 @@ include(":core:ui") include(":feature:converter") include(":feature:unitslist") include(":feature:settings") +include(":feature:tools") +include(":feature:epoch")