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" }