From ad4efb1cbd32ef3cdceb71ba57cf9e033f7926a7 Mon Sep 17 00:00:00 2001 From: Digitool Solutions Date: Mon, 11 May 2026 21:34:41 +0700 Subject: [PATCH] Migrate UI from android project to kmp --- composeApp/build.gradle.kts | 9 + .../drawable/baseline_add_24.xml | 5 + .../drawable/baseline_arrow_back_24.xml | 5 + .../drawable/baseline_close_24.xml | 5 + .../drawable/baseline_filter_list_24.xml | 5 + .../drawable/baseline_menu_24.xml | 5 + .../drawable/baseline_warning_24.xml | 5 + .../composeResources/drawable/dts_text.png | Bin 0 -> 20825 bytes .../drawable/ic_arrow_right.xml | 9 + .../composeResources/drawable/ic_check.xml | 9 + .../composeResources/drawable/ic_cross.xml | 9 + .../composeResources/drawable/ic_dot.xml | 9 + .../drawable/ic_signal_max.xml | 15 ++ .../drawable/ic_signal_medium.xml | 15 ++ .../drawable/ic_signal_min.xml | 15 ++ .../composeResources/values/strings.xml | 34 +++ .../app/torquevaultkmp/App.kt | 49 +--- .../app/torquevaultkmp/components/AppBar.kt | 113 +++++++++ .../app/torquevaultkmp/components/BigIcon.kt | 43 ++++ .../torquevaultkmp/components/CircularIcon.kt | 105 ++++++++ .../torquevaultkmp/components/FilterButton.kt | 80 ++++++ .../app/torquevaultkmp/components/Hint.kt | 46 ++++ .../app/torquevaultkmp/components/RssiIcon.kt | 71 ++++++ .../torquevaultkmp/components/WarningView.kt | 211 ++++++++++++++++ .../app/torquevaultkmp/di/Koin.kt | 4 + .../app/torquevaultkmp/di/Modules.kt | 5 + .../app/torquevaultkmp/screens/Destination.kt | 15 ++ .../app/torquevaultkmp/screens/MainScreen.kt | 92 +++++++ .../screens/auth/AuthDestination.kt | 12 + .../screens/auth/LoginScreen.kt | 232 ++++++++++++++++++ .../screens/bonded/BondedDestination.kt | 12 + .../screens/bonded/BondedScreen.kt | 108 ++++++++ .../screens/bonded/view/BondedDeviceItem.kt | 122 +++++++++ .../screens/bonded/view/EmptyBondedDevice.kt | 39 +++ .../screens/home/HomeDestination.kt | 12 + .../torquevaultkmp/screens/home/HomeScreen.kt | 104 ++++++++ .../screens/home/views/HomeEmpty.kt | 50 ++++ .../screens/home/views/HomeItem.kt | 73 ++++++ .../screens/logs/LogDestination.kt | 12 + .../screens/logs/LogFilterState.kt | 86 +++++++ .../torquevaultkmp/screens/logs/LogScreen.kt | 169 +++++++++++++ .../screens/logs/view/EmptyLog.kt | 39 +++ .../screens/logs/view/FilterBottomSheet.kt | 201 +++++++++++++++ .../screens/logs/view/LogItem.kt | 56 +++++ .../screens/settings/SettingScreen.kt | 214 ++++++++++++++++ .../screens/settings/SettingsDestination.kt | 12 + .../app/torquevaultkmp/theme/Color.kt | 19 ++ .../app/torquevaultkmp/theme/Theme.kt | 77 ++++++ .../app/torquevaultkmp/theme/Type.kt | 35 +++ gradle/libs.versions.toml | 11 + 50 files changed, 2643 insertions(+), 40 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_add_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_arrow_back_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_close_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_filter_list_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_menu_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/baseline_warning_24.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/dts_text.png create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_check.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_cross.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_dot.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_signal_max.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_signal_medium.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_signal_min.xml create mode 100644 composeApp/src/commonMain/composeResources/values/strings.xml create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/CircularIcon.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/WarningView.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/BondedDeviceItem.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/EmptyBondedDevice.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeEmpty.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeItem.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogFilterState.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/EmptyLog.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/FilterBottomSheet.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/LogItem.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingsDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Color.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Theme.kt create mode 100644 composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Type.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b696b73..9d616f4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -49,6 +49,15 @@ kotlin { implementation(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) + + implementation(libs.compose.material.icons.extended) + implementation(libs.androidx.navigation.compose) + +// implementation(libs.compose.material3.adaptiveNavigation3) +// implementation(libs.androidx.lifecycle.viewmodelNavigation3) + } + iosMain.dependencies { + } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_add_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_add_24.xml new file mode 100644 index 0000000..dda571e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_add_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_arrow_back_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_arrow_back_24.xml new file mode 100644 index 0000000..11a1702 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_arrow_back_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_close_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_close_24.xml new file mode 100644 index 0000000..34dd837 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_close_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_filter_list_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_filter_list_24.xml new file mode 100644 index 0000000..69c16a8 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_filter_list_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_menu_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_menu_24.xml new file mode 100644 index 0000000..13c1af7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_menu_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/baseline_warning_24.xml b/composeApp/src/commonMain/composeResources/drawable/baseline_warning_24.xml new file mode 100644 index 0000000..5394c14 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/baseline_warning_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/dts_text.png b/composeApp/src/commonMain/composeResources/drawable/dts_text.png new file mode 100644 index 0000000000000000000000000000000000000000..6b208141f40d16fcc3ee0ef47d94725811a9b122 GIT binary patch literal 20825 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 literal 0 HcmV?d00001 diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml b/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..583d004 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_check.xml b/composeApp/src/commonMain/composeResources/drawable/ic_check.xml new file mode 100644 index 0000000..4533650 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_cross.xml b/composeApp/src/commonMain/composeResources/drawable/ic_cross.xml new file mode 100644 index 0000000..0bfb0c6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_cross.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_dot.xml b/composeApp/src/commonMain/composeResources/drawable/ic_dot.xml new file mode 100644 index 0000000..c096210 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_dot.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_signal_max.xml b/composeApp/src/commonMain/composeResources/drawable/ic_signal_max.xml new file mode 100644 index 0000000..16c8969 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_signal_max.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_signal_medium.xml b/composeApp/src/commonMain/composeResources/drawable/ic_signal_medium.xml new file mode 100644 index 0000000..cc6a8e6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_signal_medium.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_signal_min.xml b/composeApp/src/commonMain/composeResources/drawable/ic_signal_min.xml new file mode 100644 index 0000000..02066da --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_signal_min.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..a8d5ffc --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,34 @@ + + + Back + Menu + + %d dBm + + Home + + Settings + Server URL + Token + Save Settings + Logout + Re-login Required + Login + Settings saved successfully + The server URL has changed. Please log in again to refresh your session and obtain a new token. + Dark theme + Auto-connect device + + Server URL + Ex: http://192.168.1.6:8001 or https://example.com + Username + Password + Login + + Bonded Devices + + Logs + Filters + Sort by + Clear + diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt index 9e34b32..e16b8ce 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt @@ -1,49 +1,18 @@ package com.digitoolsolutions.app.torquevaultkmp -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.safeContentPadding -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import org.jetbrains.compose.resources.painterResource - -import torquevaultkmp.composeapp.generated.resources.Res -import torquevaultkmp.composeapp.generated.resources.compose_multiplatform +import com.digitoolsolutions.android.torquevault.ui.theme.TorqueVaultTheme +import com.digitoolsolutions.app.torquevaultkmp.screens.MainScreen @Composable @Preview fun App() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.primaryContainer) - .safeContentPadding() - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") - } - } - } + TorqueVaultTheme { + MainScreen( + isLoggedIn = false, + isDarkTheme = true, + onThemeToggle = {} + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt new file mode 100644 index 0000000..08c71c5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt @@ -0,0 +1,113 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.union +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import com.digitoolsolutions.app.torquevaultkmp.theme.AppBarColor +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.baseline_add_24 +import torquevaultkmp.composeapp.generated.resources.baseline_arrow_back_24 +import torquevaultkmp.composeapp.generated.resources.baseline_menu_24 +import torquevaultkmp.composeapp.generated.resources.menu_item_accessibility +import torquevaultkmp.composeapp.generated.resources.navigation_item_accessibility + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + onNavigationButtonClick: (() -> Unit)? = null, + onHamburgerButtonClick: (() -> Unit)? = null, + showBackButton: Boolean = onNavigationButtonClick != null, + backButtonIcon: Painter = painterResource(Res.drawable.baseline_arrow_back_24), + showHamburgerButton: Boolean = onHamburgerButtonClick != null, + expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight, + windowInsets: WindowInsets = WindowInsets(0,0,0,0), +// windowInsets: WindowInsets = WindowInsets.displayCutout +//// .union(WindowInsets.statusBars) +// .union(WindowInsets.navigationBars) +// .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top), + scrollBehavior: TopAppBarScrollBehavior? = null, + actions: @Composable RowScope.() -> Unit = {}, +) { + TopAppBar( + title = title, + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = AppBarColor, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + ), + navigationIcon = { + onNavigationButtonClick?.takeIf { showBackButton }?.let { action -> + IconButton(onClick = action) { + Icon( + painter = backButtonIcon, + contentDescription = stringResource(Res.string.navigation_item_accessibility), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } ?: run { + onHamburgerButtonClick?.takeIf { showHamburgerButton }?.let { action -> + IconButton(onClick = action) { + Icon( + painter = painterResource(Res.drawable.baseline_menu_24), + contentDescription = stringResource(Res.string.menu_item_accessibility), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + }, + actions = actions, + scrollBehavior = scrollBehavior, + expandedHeight = expandedHeight, + windowInsets = windowInsets, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun AppBarPreview() { + MaterialTheme { + AppBar( + title = { Text(text = "Title") }, + actions = { + IconButton(onClick = {}) { + Icon( + painter = painterResource(Res.drawable.baseline_add_24), + contentDescription = "" + ) + } + IconButton(onClick = {}) { + Icon( + painter = painterResource(Res.drawable.baseline_menu_24), + contentDescription = "" + ) + } + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt new file mode 100644 index 0000000..0dccc47 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt @@ -0,0 +1,43 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +internal fun BigIcon( + imageVector: ImageVector, + modifier: Modifier = Modifier, + size: Dp = 80.dp, + color: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f), +) { + Image( + imageVector = imageVector, + contentDescription = null, + modifier = modifier.size(size), + colorFilter = ColorFilter.tint(color), + ) +} + +@Composable +internal fun BigIcon( + painterResource: Painter, + modifier: Modifier = Modifier, + size: Dp = 80.dp, + color: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f), +) { + Image( + painter = painterResource, + contentDescription = null, + modifier = modifier.size(size), + colorFilter = ColorFilter.tint(color), + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/CircularIcon.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/CircularIcon.kt new file mode 100644 index 0000000..626e8a7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/CircularIcon.kt @@ -0,0 +1,105 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.painterResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.ic_check + +/** + * A circular icon with a background. + * + * @param painter The painter to be used for the icon. + * @param modifier The modifier to be applied to the icon. + * @param backgroundColor The background color of the circle. + * @param enabled If false, the icon will be displayed in a disabled state. + * @param size The size of the circular background. + * @param iconSize The size of the icon inside the circle. + */ +@Composable +fun CircularIcon( + painter: Painter, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.secondary, + enabled: Boolean = true, + size: Dp = 40.dp, + iconSize: Dp = 24.dp, + iconTint: Color = MaterialTheme.colorScheme.onSecondary +) { + val padding = (size - iconSize) / 2 + Image( + painter = painter, + contentDescription = null, + colorFilter = if (enabled) { + ColorFilter.tint(iconTint) + } else { + ColorFilter.tint(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)) + }, + modifier = Modifier + .size(size) + .then(modifier) + .background( + color = if (enabled) { + backgroundColor + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + }, + shape = CircleShape + ) + .padding(padding) + ) +} + +/** + * A circular icon with a background. + * + * @param imageVector The vector image to be used for the icon. + * @param modifier The modifier to be applied to the icon. + * @param backgroundColor The background color of the circle. + * @param enabled If false, the icon will be displayed in a disabled state. + * @param size The size of the circular background. + * @param iconSize The size of the icon inside the circle. + */ +@Composable +fun CircularIcon( + imageVector: ImageVector, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.secondary, + enabled: Boolean = true, + size: Dp = 40.dp, + iconSize: Dp = 24.dp, + iconTint: Color = MaterialTheme.colorScheme.onSecondary +) { + CircularIcon( + painter = rememberVectorPainter(image = imageVector), + modifier = modifier, + backgroundColor = backgroundColor, + enabled = enabled, + iconSize = iconSize, + iconTint = iconTint + ) +} + +@Preview(showBackground = true) +@Composable +private fun CircularIconPreview() { + MaterialTheme { + CircularIcon( + painter = painterResource(Res.drawable.ic_check) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt new file mode 100644 index 0000000..6dbbb9f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt @@ -0,0 +1,80 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.baseline_close_24 + + +@Composable +fun FilterButton( + title: String, + icon: DrawableResource, + isSelected: Boolean, + containerColorEnabled: Color = Color.Unspecified, + containerColorDisabled: Color = Color.Unspecified, + onClick: () -> Unit = {}, +) { + FilterButton( + title = title, + painter = painterResource(icon), + isSelected = isSelected, + containerColorEnabled = containerColorEnabled, + containerColorDisabled = containerColorDisabled, + onClick = onClick + ) +} + +@Composable +fun FilterButton( + title: String, + painter: Painter, + isSelected: Boolean, + containerColorEnabled: Color = Color.Unspecified, + containerColorDisabled: Color = Color.Unspecified, + onClick: () -> Unit = {}, +) { + FilterChip( + selected = isSelected, + onClick = onClick, + label = { Text(title) }, + leadingIcon = { + Icon( + painter = if (isSelected) painterResource(Res.drawable.baseline_close_24) else painter, + contentDescription = null + ) + }, + colors = FilterChipDefaults.filterChipColors( + containerColor = containerColorDisabled, + selectedContainerColor = containerColorEnabled, + ), + ) +} + +@Composable +fun FilterButton( + title: String, + icon: ImageVector, + isSelected: Boolean, + containerColorEnabled: Color = Color.Unspecified, + containerColorDisabled: Color = Color.Unspecified, + onClick: () -> Unit = {}, +) { + FilterButton( + title = title, + painter = rememberVectorPainter(icon), + isSelected = isSelected, + containerColorEnabled = containerColorEnabled, + containerColorDisabled = containerColorDisabled, + onClick = onClick + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt new file mode 100644 index 0000000..91c962a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt @@ -0,0 +1,46 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign + + +@Composable +internal fun Hint( + text: String, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + style: TextStyle = MaterialTheme.typography.bodyMedium, + textAlign: TextAlign? = TextAlign.Center, +) { + Text( + text = text, + color = color, + style = style, + modifier = modifier, + textAlign = textAlign + ) +} + +@Composable +internal fun Hint( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + style: TextStyle = MaterialTheme.typography.bodyMedium, + textAlign: TextAlign? = TextAlign.Center, +) { + Text( + text = text, + color = color, + style = style, + modifier = modifier, + textAlign = textAlign + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt new file mode 100644 index 0000000..3ca6aed --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt @@ -0,0 +1,71 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.dbm +import torquevaultkmp.composeapp.generated.resources.ic_signal_max +import torquevaultkmp.composeapp.generated.resources.ic_signal_medium +import torquevaultkmp.composeapp.generated.resources.ic_signal_min + + +private const val MEDIUM_RSSI = -80 +private const val MAX_RSSI = -60 + +/** + * A component that displays an icon and a text representing the RSSI value. + * + * @param rssi The RSSI value. + */ +@Composable +fun RssiIcon(rssi: Int) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Image( + painter = painterResource(getImageRes(rssi)), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringResource(Res.string.dbm, rssi), + style = MaterialTheme.typography.bodySmall + ) + } +} +private fun getImageRes(rssi: Int): DrawableResource = when { + rssi < MEDIUM_RSSI -> Res.drawable.ic_signal_min + rssi < MAX_RSSI -> Res.drawable.ic_signal_medium + else -> Res.drawable.ic_signal_max +} + +@Preview(showBackground = true, widthDp = 520) +@Composable +private fun RssiIconPreview() { + MaterialTheme { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + RssiIcon(rssi = -30) + RssiIcon(rssi = -40) + RssiIcon(rssi = -50) + RssiIcon(rssi = -60) + RssiIcon(rssi = -70) + RssiIcon(rssi = -80) + RssiIcon(rssi = -90) + RssiIcon(rssi = -100) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/WarningView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/WarningView.kt new file mode 100644 index 0000000..ce8831c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/WarningView.kt @@ -0,0 +1,211 @@ +package com.digitoolsolutions.app.torquevaultkmp.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +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.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.painterResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.baseline_warning_24 + + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param imageVector The vector icon to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed. + * @param modifier The modifier to be applied to the layout. + * @param hintTextAlign The text alignment of the hint, defaults to [TextAlign.Center]. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + imageVector: ImageVector, + title: String, + hint: String, + modifier: Modifier = Modifier, + hintTextAlign: TextAlign? = TextAlign.Center, + content: @Composable ColumnScope.() -> Unit = {} +) { + WarningView( + painterResource = rememberVectorPainter(image = imageVector), + title = title, + hint = AnnotatedString(text = hint), + modifier = modifier, + hintTextAlign = hintTextAlign, + content = content, + ) +} + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param imageVector The vector icon to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed, as [AnnotatedString]. + * @param modifier The modifier to be applied to the layout. + * @param hintTextAlign The text alignment of the hint, defaults to [TextAlign.Center]. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + imageVector: ImageVector, + title: String, + hint: AnnotatedString, + modifier: Modifier = Modifier, + hintTextAlign: TextAlign? = TextAlign.Center, + content: @Composable ColumnScope.() -> Unit = {} +) { + WarningView( + painterResource = rememberVectorPainter(image = imageVector), + title = title, + hint = hint, + modifier = modifier, + hintTextAlign = hintTextAlign, + content = content, + ) +} + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param painterResource The painter to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed, as [AnnotatedString]. + * @param modifier The modifier to be applied to the layout. + * @param hintTextAlign The text alignment of the hint, defaults to [TextAlign.Center]. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + painterResource: Painter, + title: String, + hint: AnnotatedString, + modifier: Modifier = Modifier, + hintTextAlign: TextAlign? = TextAlign.Center, + content: @Composable ColumnScope.() -> Unit = {} +) { + WarningView( + painterResource = painterResource, + title = { Text(text = title) }, + hint = { Hint(text = hint, textAlign = hintTextAlign) }, + modifier = modifier, + content = content, + ) +} + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param painterResource The painter to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed. + * @param modifier The modifier to be applied to the layout. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + painterResource: Painter, + title: String, + hint: String, + modifier: Modifier = Modifier, + hintTextAlign: TextAlign? = TextAlign.Center, + content: @Composable ColumnScope.() -> Unit = {} +) { + WarningView( + painterResource = painterResource, + title = { Text(text = title) }, + hint = { Hint(text = hint, textAlign = hintTextAlign) }, + modifier = modifier, + content = content, + ) +} + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param imageVector The vector icon to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed. + * @param modifier The modifier to be applied to the layout. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + imageVector: ImageVector, + title: @Composable () -> Unit, + hint: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit = {} +) { + WarningView( + painterResource = rememberVectorPainter(image = imageVector), + title = title, + hint = hint, + modifier = modifier, + content = content, + ) +} + +/** + * A warning view is a component that displays a warning icon, a title, and a hint. + * + * @param painterResource The painter to be displayed. + * @param title The title of the warning. + * @param hint The hint to be displayed. + * @param modifier The modifier to be applied to the layout. + * @param content The content to be displayed below the hint. + */ +@Composable +fun WarningView( + painterResource: Painter, + title: @Composable () -> Unit, + hint: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit = {} +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BigIcon(painterResource = painterResource) + + title() + + hint() + + content() + } +} + +@Preview(showBackground = true) +@Composable +private fun WarningViewPreview() { + MaterialTheme { + WarningView( + painterResource = painterResource(Res.drawable.baseline_warning_24), + title = "Warning", + hint = "This is a warning view", + modifier = Modifier.fillMaxSize(), + ) { + Button(onClick = { /*TODO*/ }) { + Text(text = "This is content") + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt new file mode 100644 index 0000000..4327187 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt @@ -0,0 +1,4 @@ +package com.digitoolsolutions.app.torquevaultkmp.di + +class Koin { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt new file mode 100644 index 0000000..8944053 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt @@ -0,0 +1,5 @@ +package com.digitoolsolutions.app.torquevaultkmp.di + +import org.koin.dsl.module + +val homeModule = module { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt new file mode 100644 index 0000000..2ddb152 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt @@ -0,0 +1,15 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens + +import androidx.compose.ui.graphics.vector.ImageVector +import org.jetbrains.compose.resources.DrawableResource + +sealed class AppIcon { + data class Resource(val resId: DrawableResource) : AppIcon() + data class Vector(val imageVector: ImageVector) : AppIcon() +} + +open class Destinations( + val label: String, + val icon: AppIcon, + val route: String = label.lowercase().replace(" ", "_") +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt new file mode 100644 index 0000000..b568228 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt @@ -0,0 +1,92 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthDestination +import com.digitoolsolutions.app.torquevaultkmp.screens.auth.LoginScreen +import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedDestination +import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedScreen +import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination +import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeScreen +import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogDestination +import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogScreen +import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestination +import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.viewmodel.koinViewModel +@Composable +fun MainScreen( + isLoggedIn: Boolean, + isDarkTheme: Boolean, + onThemeToggle: (Boolean) -> Unit, +) { + val destinations = listOf( + HomeDestination, + BondedDestination, + LogDestination, + SettingDestination, + ) + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + if (currentDestination?.route != AuthDestination.route) + NavigationBar { + destinations.forEach { dest -> + NavigationBarItem( + icon = { + when (val icon = dest.icon) { + is AppIcon.Resource -> Icon( + painter = painterResource(icon.resId), + contentDescription = dest.label + ) + is AppIcon.Vector -> Icon( + imageVector = icon.imageVector, + contentDescription = dest.label + ) + } + }, + label = { Text(dest.label) }, + selected = currentDestination?.route == dest.route, + onClick = { + navController.navigate(dest.route) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true + } + } + ) + } + } + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = HomeDestination.route, + modifier = Modifier.padding(innerPadding).fillMaxSize() + ) { + composable(AuthDestination.route) { LoginScreen(navController) } + composable(HomeDestination.route) { HomeScreen(navController) } + composable(BondedDestination.route) { BondedScreen(navController) } + composable(LogDestination.route) { LogScreen(navController) } + composable(SettingDestination.route) { + SettingScreen(navController, isDarkTheme, onThemeToggle) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthDestination.kt new file mode 100644 index 0000000..07ccca1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthDestination.kt @@ -0,0 +1,12 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.auth + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Key +import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon +import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations + +object AuthDestination: Destinations( + label = "Login", + icon = AppIcon.Vector(Icons.Default.Key), + route = "login" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt new file mode 100644 index 0000000..e7bf1a7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt @@ -0,0 +1,232 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.auth + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.btn_login +import torquevaultkmp.composeapp.generated.resources.dts_text +import torquevaultkmp.composeapp.generated.resources.lbl_password +import torquevaultkmp.composeapp.generated.resources.lbl_server_url +import torquevaultkmp.composeapp.generated.resources.lbl_username +import torquevaultkmp.composeapp.generated.resources.server_url_placeholder + +@Composable +fun LoginScreen( + navController: NavController, +// viewModel: AuthViewModel = hiltViewModel() +) { +// val loginState by viewModel.loginState.collectAsState() + + LoginScreenContent( +// viewModel = viewModel, +// loginState = loginState, + onLogin = { serverUrl, username, password -> +// viewModel.updateServerUrl(serverUrl) +// viewModel.login(username, password) + }, + onLoginSuccess = { + navController.navigate(HomeDestination.route) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreenContent( +// viewModel: AuthViewModel, +// loginState: LoginUiState, + onLogin: (String, String, String) -> Unit, + onLoginSuccess: () -> Unit +) { + val focusManager = LocalFocusManager.current +// val serverUrl by viewModel.serverUrl.collectAsState() + val serverUrl = "" + var username by remember { mutableStateOf("admin") } + var password by remember { mutableStateOf("admin") } + var passwordVisible by remember { mutableStateOf(false) } + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + +// LaunchedEffect(loginState) { +// if (loginState is LoginUiState.Error) { +// snackbarHostState.showSnackbar(loginState.message) +// } +// if (loginState is LoginUiState.Success) { +// onLoginSuccess() +// } +// } + Scaffold( + modifier = Modifier.imePadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }) + { padding -> + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + .padding(padding) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .background(Color.White, shape = RoundedCornerShape(8.dp)) + .padding(4.dp) + .height(70.dp) + .width(190.dp), + contentAlignment = Alignment.Center + ){ + Image( + painter = painterResource(Res.drawable.dts_text), + contentDescription = "App Logo", + modifier = Modifier.fillMaxSize() + ) + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ), + value = serverUrl, + onValueChange = { /** viewModel.onChangeServerUrl(it) */ }, + label = { Text(stringResource(Res.string.lbl_server_url)) }, + placeholder = { Text(stringResource(Res.string.server_url_placeholder)) } + + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ), + value = username, + onValueChange = { username = it }, + label = { Text(stringResource(Res.string.lbl_username)) } + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = + if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = image, + contentDescription = if (passwordVisible) "Hide password" else "Show password" + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + if (username.isBlank() || password.isBlank()) + scope.launch { + snackbarHostState.showSnackbar("Username and Password is required!") + } + else + onLogin(serverUrl, username, password) + } + ), + value = password, + onValueChange = { password = it }, + label = { Text(stringResource(Res.string.lbl_password)) } + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + modifier = Modifier + .width(150.dp) + .height(45.dp), + onClick = { + if (username.isBlank() || password.isBlank()) + scope.launch { + snackbarHostState.showSnackbar("Username and password is required!") + } + else + onLogin(serverUrl.trim(), username.trim(), password.trim()) + }, +// enabled = loginState !is LoginUiState.Loading + ) { + Text(stringResource(Res.string.btn_login)) +// if (loginState is LoginUiState.Loading) { +// Spacer(modifier = Modifier.width(16.dp)) +// CircularProgressIndicator( +// modifier = Modifier.size(20.dp), +// color = Color.Blue, +// strokeWidth = 2.dp +// ) +// +// } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LoginScreenPreview() { + LoginScreenContent( +// viewModel = viewModel(), +// loginState = LoginUiState.Idle, + onLogin = { _, _, _ -> }, + onLoginSuccess = {} + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedDestination.kt new file mode 100644 index 0000000..e6f4f7d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedDestination.kt @@ -0,0 +1,12 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.bonded + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Link +import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon +import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations + +object BondedDestination: Destinations( + label = "Bonded", + icon = AppIcon.Vector(Icons.Outlined.Link), + route = "bonded" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt new file mode 100644 index 0000000..3a2a694 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt @@ -0,0 +1,108 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.bonded + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import kotlin.uuid.ExperimentalUuidApi +import com.digitoolsolutions.app.torquevaultkmp.components.AppBar +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.bonded_title + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) +@Composable +fun BondedScreen( + navController: NavController, +// bleScannerViewModel: ScannerViewModel, +// viewModel: BondedViewModel = hiltViewModel() +) { +// val devices = viewModel.bondedDevices.collectAsLazyPagingItems() + + BondedContent( +// devices = devices, +// onRefresh = {devices.refresh()}, +// onDelete = { +// viewModel.removeDevice(it) +// bleScannerViewModel.removeBondedCache(it.id) +// } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun BondedContent( +// devices: LazyPagingItems, +// onRefresh: () -> Unit, +// onDelete: (Device) -> Unit +) { + val snackbarHostState = remember { SnackbarHostState() } + Scaffold ( + modifier = Modifier.imePadding(), + topBar = { + AppBar( + title = {Text(stringResource(Res.string.bonded_title))}, + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }) + { padding -> + val pullToRefreshState = rememberPullToRefreshState() + val scope = rememberCoroutineScope() + PullToRefreshBox( + isRefreshing = false, + onRefresh = { +// onRefresh() + scope.launch { + pullToRefreshState.animateToHidden() + } + }, + state = pullToRefreshState, + modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(horizontal = 16.dp, vertical = 12.dp), + ){ +// if (devices.itemCount == 0) { +// EmptyBondedView() +// }else { +// LazyColumn{ +// items(count = devices.itemCount, key = { index -> devices[index]?.id ?: index }) { index -> +// val device = devices[index] +// device?.let { it -> +// BondedDeviceItem(it, onDelete = { onDelete(it) }) +// } +// } +// } +// } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LogScreenPreview() { +// val fakeDevices = listOf( +// Device("AA:BB:CC:DD:EE", "Device 1"), +// Device("AA:BB:CC:DD:FF", "Device 2") +// ) +// val pagingData = PagingData.from(fakeDevices) +// val lazyPagingItems = flowOf(pagingData).collectAsLazyPagingItems() +// +// BondedContent( +// devices = lazyPagingItems, +// onRefresh = {lazyPagingItems.refresh()}, +// onDelete = {} +// ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/BondedDeviceItem.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/BondedDeviceItem.kt new file mode 100644 index 0000000..a54d9b3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/BondedDeviceItem.kt @@ -0,0 +1,122 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.DeleteSweep +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxDefaults +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +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.lerp +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon + +@Composable +fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier: Modifier = Modifier) { + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( + SwipeToDismissBoxValue.Settled, + SwipeToDismissBoxDefaults.positionalThreshold + ) + SwipeToDismissBox( + state = swipeToDismissBoxState, + modifier = modifier.fillMaxSize().padding(vertical = 4.dp), + enableDismissFromStartToEnd = false, + backgroundContent = { + when (swipeToDismissBoxState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + SwipeToDismissBackgroundItem(label = "Swipe to delete item", contentAlignment = Alignment.CenterStart, state = swipeToDismissBoxState) + } + SwipeToDismissBoxValue.EndToStart -> {SwipeToDismissBackgroundItem(label = "Swipe to delete item", state = swipeToDismissBoxState)} + SwipeToDismissBoxValue.Settled -> {} + } + }, +// onDismiss = {onDelete(device)}, + ) { + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Row ( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularIcon( imageVector = Icons.Default.Bluetooth, backgroundColor = Color(0xFF0082FC)) + Spacer(modifier = Modifier.width(16.dp)) + Column { +// Text( +// text = device.name, +// style = MaterialTheme.typography.titleMedium +// ) +// Text( +// text = device.id, +// style = MaterialTheme.typography.bodyMedium, +// color = MaterialTheme.colorScheme.onSurfaceVariant +// ) + } + } + } + } +} + +@Composable +internal fun SwipeToDismissBackgroundItem( + label: String, + contentAlignment: Alignment = Alignment.CenterEnd, + icon: @Composable () -> Unit = { + Icon( + imageVector = Icons.Default.DeleteSweep, + contentDescription = "Delete", + tint = Color.White + ) + }, + state: SwipeToDismissBoxState +) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(12.dp)) + .background(lerp(Color.LightGray, Color.Red, state.progress)) + .padding(end = 20.dp), + contentAlignment = contentAlignment + ) { + Row { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.width(8.dp)) + icon() + } + } +} + +@Preview +@Composable +fun BondedDeviceItemPreview() { + BondedDeviceItem( +// Device("AA:BB:CC:DD:EE", "Device 1"), +// onDelete = {} + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/EmptyBondedDevice.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/EmptyBondedDevice.kt new file mode 100644 index 0000000..04b5244 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/view/EmptyBondedDevice.kt @@ -0,0 +1,39 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view + +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.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BluetoothDisabled +import androidx.compose.material3.Icon +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.unit.dp + +@Composable +fun EmptyBondedView() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.BluetoothDisabled, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "No bonded devices found", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeDestination.kt new file mode 100644 index 0000000..ffd8201 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeDestination.kt @@ -0,0 +1,12 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.home + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Home +import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon +import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations + +object HomeDestination: Destinations( + label = "Home", + icon = AppIcon.Vector(Icons.Outlined.Home), + route = "home" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt new file mode 100644 index 0000000..3af0187 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt @@ -0,0 +1,104 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.home + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.outlined.SyncAlt +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxDefaults +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.platform.LocalContext +//import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.digitoolsolutions.app.torquevaultkmp.components.AppBar +import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeEmpty +import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeItem +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.home_title +//import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +//import androidx.navigation.NavController +//import com.digitoolsolutions.android.components.ui.AppBar +//import com.digitoolsolutions.android.components.ui.CircularIcon +//import com.digitoolsolutions.android.home.view.HomeEmpty +//import com.digitoolsolutions.android.home.view.HomeItem +//import com.digitoolsolutions.android.scanner.viewmodel.ScannerViewModel +//import com.digitoolsolutions.android.uart.spec.ConnectionState +//import com.digitoolsolutions.android.uart.spec.UartDevice +import kotlin.uuid.ExperimentalUuidApi + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) +@Composable +fun HomeScreen( + navController: NavController, +// bleScannerViewModel: ScannerViewModel +) { + val snackbarHostState = remember { SnackbarHostState() } +// val connectedDevice by bleScannerViewModel.connectedDevices.collectAsState(initial = emptyList()) + + Scaffold ( + modifier = Modifier.imePadding(), + topBar = { + AppBar( + title = {Text(stringResource(Res.string.home_title))}, + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }) + { padding -> + Column ( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { +// if(connectedDevice.isEmpty()) +// HomeEmpty() +// else +// LazyColumn { +// items(connectedDevice) { device -> +// HomeItem(device) +// } +// } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HomeScreenPreview() { +// HomeScreen( +// navController = NavController(LocalContext.current), +//// bleScannerViewModel = hiltViewModel() +// ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeEmpty.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeEmpty.kt new file mode 100644 index 0000000..b2fb9e5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeEmpty.kt @@ -0,0 +1,50 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.home.views + +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.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.BluetoothConnected +import androidx.compose.material3.Icon +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun HomeEmpty() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Outlined.BluetoothConnected, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "No active devices", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Currently no devices are connected.", + style = MaterialTheme.typography.bodyMedium + ) + } +} + +@Preview(showBackground = true) +@Composable +fun HomeEmptyPreview() { + HomeEmpty() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeItem.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeItem.kt new file mode 100644 index 0000000..d5b304b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/views/HomeItem.kt @@ -0,0 +1,73 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.home.views + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.outlined.SyncAlt +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon + +//import com.digitoolsolutions.android.components.ui.CircularIcon +//import com.digitoolsolutions.android.uart.spec.ConnectionState +//import com.digitoolsolutions.android.uart.spec.UartDevice + + +@Composable +//fun HomeItem(device: UartDevice, modifier: Modifier = Modifier) { +fun HomeItem(device: Any, modifier: Modifier = Modifier) { + OutlinedCard( + modifier = modifier.fillMaxWidth().padding(vertical = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Row ( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularIcon(Icons.Outlined.SyncAlt) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { +// device.name?.let { +// Text( +// text = it, +// style = MaterialTheme.typography.titleMedium +// ) +// } +// Text( +// text = device.identifier, +// style = MaterialTheme.typography.bodyMedium, +// color = MaterialTheme.colorScheme.onSurfaceVariant +// ) + } + Spacer(modifier = Modifier.width(16.dp)) +// val statusColor = when (device.status) { +// ConnectionState.CONNECTING -> MaterialTheme.colorScheme.onSurfaceVariant +// ConnectionState.READY -> Color.Green +// else -> MaterialTheme.colorScheme.error +// } +// Text( +// text = device.status.name, +// style = MaterialTheme.typography.bodyMedium, +// color = statusColor +// ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ItemPreview() { +// HomeItem(device = UartDevice("AA:BB:CC:DD:EE", "Device 1")) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogDestination.kt new file mode 100644 index 0000000..9bbe0fd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogDestination.kt @@ -0,0 +1,12 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.logs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon +import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations + +object LogDestination: Destinations( + label = "Logs", + icon = AppIcon.Vector(Icons.AutoMirrored.Filled.List), + route = "logs" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogFilterState.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogFilterState.kt new file mode 100644 index 0000000..3698a27 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogFilterState.kt @@ -0,0 +1,86 @@ +//package com.digitoolsolutions.app.torquevaultkmp.screens.logs +// +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.Stable +//import androidx.compose.runtime.getValue +//import androidx.compose.runtime.mutableStateOf +//import androidx.compose.runtime.saveable.Saver +//import androidx.compose.runtime.saveable.rememberSaveable +//import androidx.compose.runtime.setValue +//import com.digitoolsolutions.android.storage.model.LogFilter +// +//@Stable +//interface LogFilterState { +// val filters: List +//// val sortingOptions: List +// val activeFilter: LogFilter +//// val activeSort: LogSortOption +// val isEmpty: Boolean +// +// fun setFilter(filter: LogFilter) +//// fun sortBy(option: LogSortOption) +// fun clear() +//} +// +//val logFilterSaver = Saver( +// save = { filter -> +// when (filter) { +// LogFilter.All -> "All" +// LogFilter.System -> "System" +// LogFilter.Device -> "Device" +// is LogFilter.ByDeviceId -> "ByDeviceId:${filter.deviceId}" +// } +// }, +// restore = { str -> +// when { +// str == "All" -> LogFilter.All +// str == "System" -> LogFilter.System +// str == "Device" -> LogFilter.Device +// str.startsWith("ByDeviceId:") -> LogFilter.ByDeviceId(str.removePrefix("ByDeviceId:")) +// else -> LogFilter.All +// } +// } +//) +// +////val logSortSaver = Saver( +//// save = { sort -> +//// when (sort) { +//// LogSortOption.ByDate -> "ByDate" +//// } +//// }, +//// restore = { str -> +//// when (str) { +//// "ByDate" -> LogSortOption.ByDate +//// else -> LogSortOption.ByDate +//// } +//// } +////) +// +// +//@Composable +//fun rememberLogFilterState( +// filters: List = listOf(LogFilter.All, LogFilter.System, LogFilter.Device), +//// sortingOptions: List = listOf() +//): LogFilterState { +// var selectedFilter by rememberSaveable(stateSaver = logFilterSaver) { +// mutableStateOf(LogFilter.All) +// } +//// var selectedSort by rememberSaveable(stateSaver = logSortSaver) { +//// mutableStateOf(LogSortOption.ByDate) +//// } +// +// return object : LogFilterState { +// override val filters = filters +//// override val sortingOptions = sortingOptions +// override val activeFilter: LogFilter = selectedFilter +//// override val activeSort: LogSortOption = selectedSort +// override val isEmpty: Boolean get() = selectedFilter == LogFilter.All // && selectedSort == LogSortOption.ByDate +// +// override fun setFilter(filter: LogFilter) { selectedFilter = filter } +//// override fun sortBy(option: LogSortOption) { selectedSort = option } +// override fun clear() { +// selectedFilter = LogFilter.All +//// selectedSort = LogSortOption.ByDate +// } +// } +//} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt new file mode 100644 index 0000000..7ad677f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt @@ -0,0 +1,169 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.logs + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSheet +import com.digitoolsolutions.app.torquevaultkmp.components.AppBar +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.log_title +import kotlin.uuid.ExperimentalUuidApi + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) +@Composable +fun LogScreen( + navController: NavController, +// viewModel: LogViewModel = hiltViewModel() +) { +// val state = rememberLogFilterState() +// val logs = viewModel.getLogs(filter = state.activeFilter).collectAsLazyPagingItems() + + LogContent( +// state = state, +// logs = logs, +// onRefresh = {logs.refresh()}, +// onDeleteAllLogs = {viewModel.removeAllLog()}, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun LogContent( +// state: LogFilterState, +// logs: LazyPagingItems, +// onRefresh: () -> Unit, +// onDeleteAllLogs: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + var showConfirmDialog by remember { mutableStateOf(false) } + var showBottomSheet by remember { mutableStateOf(false) } + + Scaffold ( + modifier = Modifier.imePadding(), + topBar = { + AppBar( + title = {Text(stringResource(Res.string.log_title))}, + actions = { + IconButton(onClick = {showConfirmDialog = true}){ + Icon( + Icons.Default.DeleteForever, + contentDescription = "Delete all log", + tint = Color.Red + ) + } + IconButton(onClick = {showBottomSheet = true}){ + Icon( + Icons.Outlined.FilterList, + contentDescription = "Filter logs", + tint = Color.White + ) + } + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }) + { padding -> + val pullToRefreshState = rememberPullToRefreshState() + val scope = rememberCoroutineScope() + PullToRefreshBox( + isRefreshing = false, + onRefresh = { +// onRefresh() + scope.launch { + pullToRefreshState.animateToHidden() + } + }, + state = pullToRefreshState, + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .padding(horizontal = 16.dp, vertical = 12.dp), + ){ +// if (logs.itemCount == 0) { +// EmptyLogView() +// }else { +// LazyColumn{ +// items(logs.itemCount, key = { index -> logs[index]?.id ?: index }) { index -> +// val log = logs[index] +// log?.let { it -> +// LogItem(it) +// } +// } +// } +// } + } + if (showConfirmDialog) { + AlertDialog( + onDismissRequest = { showConfirmDialog = false }, + title = { Text("Delete Logs") }, + text = { Text("Warning: This will permanently delete all current logs. This action cannot be undone.") }, + confirmButton = { + TextButton(onClick = { +// onDeleteAllLogs() + showConfirmDialog = false + }) { + Text("Delete", color = Color.Red) + } + }, + dismissButton = { + TextButton(onClick = { showConfirmDialog = false }) { + Text("Cancel", color = Color.White) + } + } + ) + } + if (showBottomSheet) { + FilterBottomSheet( +// state = state, + onDismissRequest = {showBottomSheet = false} + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LogScreenPreview() { +// val fakeDevices = listOf( +// Log(1, "Log 1", "This is log 1"), +// Log(2, "Log 2", "This is log 2"), +// Log(3, "Log 3", "This is log 3"), +// ) +// val pagingData = PagingData.from(fakeDevices) +// val lazyPagingItems = flowOf(pagingData).collectAsLazyPagingItems() + + LogContent( +// state = rememberLogFilterState(), +// logs = lazyPagingItems, +// onRefresh = {lazyPagingItems.refresh()}, +// onDeleteAllLogs = {} + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/EmptyLog.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/EmptyLog.kt new file mode 100644 index 0000000..9bc21df --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/EmptyLog.kt @@ -0,0 +1,39 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.logs.view + +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.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.NoteAlt +import androidx.compose.material3.Icon +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.unit.dp + +@Composable +fun EmptyLogView() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Outlined.NoteAlt, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(64.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Empty logs", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/FilterBottomSheet.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/FilterBottomSheet.kt new file mode 100644 index 0000000..d0991b9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/FilterBottomSheet.kt @@ -0,0 +1,201 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.logs.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.clear + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterBottomSheet( +// state: LogFilterState, + onDismissRequest: () -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { + onDismissRequest() + }, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + containerColor = MaterialTheme.colorScheme.surface, + tonalElevation = 16.dp, + dragHandle = { + Box( + modifier = Modifier + .padding(8.dp) + .width(50.dp) + .height(6.dp) + .clip(RoundedCornerShape(50)) + .background(Color.White) + ) + } + ) { + FilterContent(/** state = state,*/ onDismissRequest = onDismissRequest) + } +} + +@Composable +internal fun FilterContent( +// state: LogFilterState, + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, +) { + Column( + modifier = modifier, + ) { + Header( + modifier = Modifier.padding(horizontal = 16.dp), + text = "Filter", + isSelectedFilterEmpty = true ,// state.isEmpty, + onFilterReset = { +// state.clear() + onDismissRequest() + } + ) + +// if (state.filters.isNotEmpty()) { +// FlowRow( +// modifier = Modifier.padding(horizontal = 8.dp), +// horizontalArrangement = Arrangement.spacedBy(8.dp), +// verticalArrangement = Arrangement.spacedBy(0.dp), +// ) { +// state.filters.forEach { filter -> +// Row( +// modifier = Modifier +// .clip(RoundedCornerShape(8.dp)) +// .selectable( +// selected = state.activeFilter == filter, +// onClick = { state.setFilter(filter) }, +// role = Role.RadioButton +// ) +// .padding(end = 16.dp), +// verticalAlignment = Alignment.CenterVertically +// ) { +// RadioButton( +// selected = state.activeFilter == filter, +// onClick = { state.setFilter(filter) } +// ) +// Text(filter.toString().replaceFirstChar { it.uppercase() }) +// } +// } +// } +// } + +// if (state.filters.isNotEmpty() && state.sortingOptions.isNotEmpty()) { +// HorizontalDivider() +// +// Text( +// text = "Sort by", +// style = MaterialTheme.typography.titleMedium, +// modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp) +// ) +// } +// +// if (state.sortingOptions.isNotEmpty()) { +// state.sortingOptions.forEach { sort -> +// Row( +// modifier = Modifier +// .clip(RoundedCornerShape(8.dp)) +// .selectable( +// selected = state.activeSort == sort, +// onClick = { state.sortBy(sort) }, +// role = Role.RadioButton +// ) +// .padding(horizontal = 16.dp), +// verticalAlignment = Alignment.CenterVertically +// ) { +// RadioButton( +// selected = state.activeSort == sort, +// onClick = { state.sortBy(sort) } +// ) +// Text(sort.toString().replaceFirstChar { it.uppercase() }) +// } +// } +// } + } +} + +@Composable +internal fun Header( + text: String, + isSelectedFilterEmpty: Boolean, + modifier: Modifier = Modifier, + onFilterReset: () -> Unit +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .weight(1f) + ) + TextButton( + onClick = onFilterReset, + enabled = !isSelectedFilterEmpty, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Delete, + contentDescription = null, + ) + Text( + text = stringResource(Res.string.clear), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun FilterBottomSheetPreview() { + FilterBottomSheet( +// state = rememberLogFilterState(), + onDismissRequest = {} + ) +} + +@Preview(showBackground = true) +@Composable +fun HeaderPreview(){ + Header(text = "Filter", isSelectedFilterEmpty = true, onFilterReset = {}) +} + +@Preview(showBackground = true) +@Composable +fun FilterContentPreview(){ +// FilterContent(state = rememberLogFilterState(), onDismissRequest = {}) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/LogItem.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/LogItem.kt new file mode 100644 index 0000000..491bbe9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/view/LogItem.kt @@ -0,0 +1,56 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.logs.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +//import com.digitoolsolutions.android.storage.data.local.db.entity.Log +//import com.digitoolsolutions.android.storage.model.LogFilter +//import java.time.Instant +//import java.time.ZoneId +//import java.time.format.DateTimeFormatter + +@Composable +fun LogItem(/** log: Log,*/ modifier: Modifier = Modifier) { +// val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss") +// .withZone(ZoneId.systemDefault()) +// val formattedTime = formatter.format(Instant.ofEpochMilli(log.timestamp)) + + Column( + modifier = Modifier.fillMaxWidth(), + ){ +// Column( +// modifier = Modifier.padding(16.dp), +// ) { +// Text( +// text = formattedTime, +// style = MaterialTheme.typography.bodySmall +// ) +// Text( +// text = log.title, +// style = MaterialTheme.typography.titleMedium +// ) +// Text( +// text = "${if(log.source == LogFilter.Device.toString()) log.deviceId ?: "Unknown" else log.source.replaceFirstChar{it.uppercase()}} - ${log.content}", +// style = MaterialTheme.typography.bodyMedium, +// color = MaterialTheme.colorScheme.onSurfaceVariant +// ) +// } + HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) + } +} + +@Preview +@Composable +fun BondedDeviceItemPreview() { + LogItem( +// Log( 1, "Log 1", "This is a log"), + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingScreen.kt new file mode 100644 index 0000000..9eabec3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingScreen.kt @@ -0,0 +1,214 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.digitoolsolutions.app.torquevaultkmp.components.AppBar +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.auto_connect +import torquevaultkmp.composeapp.generated.resources.dark_theme +import torquevaultkmp.composeapp.generated.resources.exit +import torquevaultkmp.composeapp.generated.resources.logout +import torquevaultkmp.composeapp.generated.resources.need_restart +import torquevaultkmp.composeapp.generated.resources.restart_app_msg +import torquevaultkmp.composeapp.generated.resources.save_settings +import torquevaultkmp.composeapp.generated.resources.server_url +import torquevaultkmp.composeapp.generated.resources.server_url_placeholder +import torquevaultkmp.composeapp.generated.resources.setting_title +import torquevaultkmp.composeapp.generated.resources.settings_saved_successfully +import torquevaultkmp.composeapp.generated.resources.token + +@Composable +fun SettingScreen( + navController: NavController, + isDarkTheme: Boolean, + onThemeToggle: (Boolean) -> Unit, +// viewModel: SettingViewModel = hiltViewModel() +) { +// val serverUrl by viewModel.serverUrl.collectAsState() +// val token by viewModel.refreshToken.collectAsState() +// val autoConnect by viewModel.autoConnect.collectAsState() + val serverUrl = "" + val token = "" + val autoConnect = "" + + SettingContent( + isDarkTheme = isDarkTheme, + onThemeToggle = onThemeToggle, + serverUrl = serverUrl, + onServerUrlChange = { /** viewModel.updateServerUrl(it) */ }, + token = token, + autoConnect = autoConnect == "true", + onTokenChange = { /** viewModel.updateRefreshToken(it) */ }, + onSaveSettings = { /** viewModel.saveSettings() */ }, + onLogout = { +// viewModel.logout() +// navController.navigate(AuthDestination.route) + }, + onChangeAutoConnect = { /** viewModel.updateAutoConnect(it) */ } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingContent( + isDarkTheme: Boolean, + onThemeToggle: (Boolean) -> Unit, + serverUrl: String, + onServerUrlChange: (String) -> Unit, + token: String, + autoConnect: Boolean, + onTokenChange: (String) -> Unit, + onSaveSettings: () -> Unit, + onLogout: () -> Unit, + onChangeAutoConnect: (Boolean) -> Unit +) { +// val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + // remember the initial serverUrl to check if it has changed when saving + val oldUrl by remember { mutableStateOf(serverUrl) } + var showRestartDialog by remember { mutableStateOf(false) } + val message = stringResource(Res.string.settings_saved_successfully) + + if (showRestartDialog) { + AlertDialog( + onDismissRequest = { }, + title = { Text(stringResource(Res.string.need_restart)) }, + text = { Text(stringResource(Res.string.restart_app_msg)) }, + confirmButton = { + TextButton (onClick = { onLogout() }) { + Text(stringResource(Res.string.exit)) + } + } + ) + } + + Scaffold ( + modifier = Modifier.imePadding(), + topBar = { + AppBar( + title = {Text(stringResource(Res.string.setting_title))}, + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }) + { padding -> + Column ( + modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.dark_theme)) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = isDarkTheme, + onCheckedChange = { onThemeToggle(it) } + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.auto_connect)) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = autoConnect, + onCheckedChange = { onChangeAutoConnect(it) } + ) + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = serverUrl, + onValueChange = { onServerUrlChange(it) }, + label = { Text(stringResource(Res.string.server_url)) }, + placeholder = { Text(stringResource(Res.string.server_url_placeholder)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = token, + onValueChange = { onTokenChange(it) }, + label = { Text(stringResource(Res.string.token)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + readOnly = true +// visualTransformation = PasswordVisualTransformation(), + ) + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + onSaveSettings() + if(oldUrl.trimEnd('/') != serverUrl.trimEnd('/')) + showRestartDialog = true + else + scope.launch { + snackbarHostState.showSnackbar(message) + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.save_settings)) + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + onLogout() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.logout)) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SettingScreenPreview() { + SettingContent( + isDarkTheme = true, + onThemeToggle = {}, + serverUrl = "https://example.com", + onServerUrlChange = {}, + token = "dummy_token", + onTokenChange = {}, + onSaveSettings = {}, + onLogout = {}, + autoConnect = false, + onChangeAutoConnect = {}, + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingsDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingsDestination.kt new file mode 100644 index 0000000..b6273de --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingsDestination.kt @@ -0,0 +1,12 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.settings + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Settings +import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon +import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations + +object SettingDestination: Destinations( + label = "Settings", + icon = AppIcon.Vector(Icons.Outlined.Settings), + route = "settings" +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Color.kt new file mode 100644 index 0000000..c4b2936 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Color.kt @@ -0,0 +1,19 @@ +package com.digitoolsolutions.app.torquevaultkmp.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFFFBCF0) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val AppBarColor = Color(0xFF00A9CE) +val ColorError = Color(0xFFBA1B1B) +val ColorWarning = Color(0xFFF1750B) +val ColorSuccess = Color(0xFF008D45) +val ColorNeutral = Color(0xFF70787C) +val ColorNordicLogo = Color(0xFF00A9CE) +val ColorNordicLogoText = Color.Black diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Theme.kt new file mode 100644 index 0000000..82e90a0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Theme.kt @@ -0,0 +1,77 @@ +package com.digitoolsolutions.android.torquevault.ui.theme + +//import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +//import androidx.compose.material3.dynamicDarkColorScheme +//import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.platform.LocalContext +import com.digitoolsolutions.app.torquevaultkmp.theme.Typography + + +//private val DarkColorScheme = darkColorScheme( +// primary = Color(0xFF8B0000), +// secondary = Color(0xFF616161), +// tertiary = Color(0xFFFFA000) +//) +// +//private val LightColorScheme = lightColorScheme( +// primary = Purple40, +// secondary = PurpleGrey40, +// tertiary = Pink40 +// +// +//) +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFFB71C1C), + secondary = Color(0xFF424242), + tertiary = Color(0xFFFFA000), + background = Color(0xFF121212), + surface = Color(0xFF1E1E1E), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = Color.White, + onSurface = Color.White +) + +private val LightColorScheme = lightColorScheme( + primary = Color(0xFFB71C1C), + secondary = Color(0xFF616161), + tertiary = Color(0xFFFFC107), + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F) +) + +@Composable +fun TorqueVaultTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { +// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { +// val context = LocalContext.current +// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) +// } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Type.kt new file mode 100644 index 0000000..1d942d5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/theme/Type.kt @@ -0,0 +1,35 @@ +package com.digitoolsolutions.app.torquevaultkmp.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e6750d..5ebee39 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,11 @@ junit = "4.13.2" kotlin = "2.3.21" material3 = "1.10.0-alpha05" koin-bom = "4.2.1" +material-icons-extended = "1.7.3" +navigation-compose = "2.9.2" +compose-multiplatform-adaptive = "1.3.0-alpha07" +compose-multiplatform-lifecycle = "2.10.0" + [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -27,9 +32,12 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose"} + compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } +compose-material-icons-extended = {group = "org.jetbrains.compose.material", name = "material-icons-extended", version.ref = "material-icons-extended" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } @@ -43,6 +51,9 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } koin-ktor = { module = "io.insert-koin:koin-ktor" } koin-test = { module = "io.insert-koin:koin-test" } +#compose-material3-adaptiveNavigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "compose-multiplatform-adaptive" } +#androidx-lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-multiplatform-lifecycle" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }