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 0000000..6b20814 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/dts_text.png differ 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" }