Migrate UI from android project to kmp

This commit is contained in:
Digitool Solutions
2026-05-11 21:34:41 +07:00
parent 80a0a1c193
commit ad4efb1cbd
50 changed files with 2643 additions and 40 deletions
+9
View File
@@ -49,6 +49,15 @@ kotlin {
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel) 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 { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFFF" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FFFFFF" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M10,17l5,-5 -5,-5v10z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41L9,16.17z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="24dp"
android:viewportWidth="34"
android:viewportHeight="24">
<path
android:pathData="M2,8L6,8A2,2 0,0 1,8 10L8,22A2,2 0,0 1,6 24L2,24A2,2 0,0 1,0 22L0,10A2,2 0,0 1,2 8z"
android:fillColor="#008d45"/>
<path
android:pathData="M15,4L19,4A2,2 0,0 1,21 6L21,22A2,2 0,0 1,19 24L15,24A2,2 0,0 1,13 22L13,6A2,2 0,0 1,15 4z"
android:fillColor="#008d45"/>
<path
android:pathData="M28,0L32,0A2,2 0,0 1,34 2L34,22A2,2 0,0 1,32 24L28,24A2,2 0,0 1,26 22L26,2A2,2 0,0 1,28 0z"
android:fillColor="#008d45"/>
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="24dp"
android:viewportWidth="34"
android:viewportHeight="24">
<path
android:pathData="M2,8L6,8A2,2 0,0 1,8 10L8,22A2,2 0,0 1,6 24L2,24A2,2 0,0 1,0 22L0,10A2,2 0,0 1,2 8z"
android:fillColor="#f1750b"/>
<path
android:pathData="M15,4L19,4A2,2 0,0 1,21 6L21,22A2,2 0,0 1,19 24L15,24A2,2 0,0 1,13 22L13,6A2,2 0,0 1,15 4z"
android:fillColor="#f1750b"/>
<path
android:pathData="M28,0L32,0A2,2 0,0 1,34 2L34,22A2,2 0,0 1,32 24L28,24A2,2 0,0 1,26 22L26,2A2,2 0,0 1,28 0z"
android:fillColor="#70787C"/>
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="24dp"
android:viewportWidth="34"
android:viewportHeight="24">
<path
android:pathData="M2,8L6,8A2,2 0,0 1,8 10L8,22A2,2 0,0 1,6 24L2,24A2,2 0,0 1,0 22L0,10A2,2 0,0 1,2 8z"
android:fillColor="#BA1B1B"/>
<path
android:pathData="M15,4L19,4A2,2 0,0 1,21 6L21,22A2,2 0,0 1,19 24L15,24A2,2 0,0 1,13 22L13,6A2,2 0,0 1,15 4z"
android:fillColor="#70787C"/>
<path
android:pathData="M28,0L32,0A2,2 0,0 1,34 2L34,22A2,2 0,0 1,32 24L28,24A2,2 0,0 1,26 22L26,2A2,2 0,0 1,28 0z"
android:fillColor="#70787C"/>
</vector>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="navigation_item_accessibility">Back</string>
<string name="menu_item_accessibility">Menu</string>
<string name="dbm">%d&#xA0;dBm</string>
<string name="home_title">Home</string>
<string name="setting_title">Settings</string>
<string name="server_url">Server URL</string>
<string name="token">Token</string>
<string name="save_settings">Save Settings</string>
<string name="logout">Logout</string>
<string name="need_restart">Re-login Required</string>
<string name="exit">Login</string>
<string name="settings_saved_successfully">Settings saved successfully</string>
<string name="restart_app_msg">The server URL has changed. Please log in again to refresh your session and obtain a new token.</string>
<string name="dark_theme">Dark theme</string>
<string name="auto_connect">Auto-connect device</string>
<string name="lbl_server_url">Server URL</string>
<string name="server_url_placeholder">Ex: http://192.168.1.6:8001 or https://example.com</string>
<string name="lbl_username">Username</string>
<string name="lbl_password">Password</string>
<string name="btn_login">Login</string>
<string name="bonded_title">Bonded Devices</string>
<string name="log_title">Logs</string>
<string name="filter_title">Filters</string>
<string name="sort_by_title">Sort by</string>
<string name="clear">Clear</string>
</resources>
@@ -1,49 +1,18 @@
package com.digitoolsolutions.app.torquevaultkmp package com.digitoolsolutions.app.torquevaultkmp
import androidx.compose.animation.AnimatedVisibility import androidx.compose.runtime.Composable
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.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.painterResource import com.digitoolsolutions.android.torquevault.ui.theme.TorqueVaultTheme
import com.digitoolsolutions.app.torquevaultkmp.screens.MainScreen
import torquevaultkmp.composeapp.generated.resources.Res
import torquevaultkmp.composeapp.generated.resources.compose_multiplatform
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
MaterialTheme { TorqueVaultTheme {
var showContent by remember { mutableStateOf(false) } MainScreen(
Column( isLoggedIn = false,
modifier = Modifier isDarkTheme = true,
.background(MaterialTheme.colorScheme.primaryContainer) onThemeToggle = {}
.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")
}
}
}
} }
} }
@@ -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 = ""
)
}
}
)
}
}
@@ -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),
)
}
@@ -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)
)
}
}
@@ -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
)
}
@@ -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
)
}
@@ -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)
}
}
}
@@ -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")
}
}
}
}
@@ -0,0 +1,4 @@
package com.digitoolsolutions.app.torquevaultkmp.di
class Koin {
}
@@ -0,0 +1,5 @@
package com.digitoolsolutions.app.torquevaultkmp.di
import org.koin.dsl.module
val homeModule = module { }
@@ -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(" ", "_")
)
@@ -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)
}
}
}
}
@@ -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"
)
@@ -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 = {}
)
}
@@ -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"
)
@@ -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<Device>,
// 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 = {}
// )
}
@@ -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 = {}
)
}
@@ -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
)
}
}
@@ -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"
)
@@ -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()
// )
}
@@ -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()
}
@@ -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"))
}
@@ -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"
)
@@ -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<LogFilter>
//// val sortingOptions: List<LogSortOption>
// val activeFilter: LogFilter
//// val activeSort: LogSortOption
// val isEmpty: Boolean
//
// fun setFilter(filter: LogFilter)
//// fun sortBy(option: LogSortOption)
// fun clear()
//}
//
//val logFilterSaver = Saver<LogFilter, String>(
// 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<LogSortOption, String>(
//// save = { sort ->
//// when (sort) {
//// LogSortOption.ByDate -> "ByDate"
//// }
//// },
//// restore = { str ->
//// when (str) {
//// "ByDate" -> LogSortOption.ByDate
//// else -> LogSortOption.ByDate
//// }
//// }
////)
//
//
//@Composable
//fun rememberLogFilterState(
// filters: List<LogFilter> = listOf(LogFilter.All, LogFilter.System, LogFilter.Device),
//// sortingOptions: List<LogSortOption> = listOf()
//): LogFilterState {
// var selectedFilter by rememberSaveable(stateSaver = logFilterSaver) {
// mutableStateOf<LogFilter>(LogFilter.All)
// }
//// var selectedSort by rememberSaveable(stateSaver = logSortSaver) {
//// mutableStateOf<LogSortOption>(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
// }
// }
//}
@@ -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<Log>,
// 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 = {}
)
}
@@ -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
)
}
}
@@ -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 = {})
}
@@ -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"),
)
}
@@ -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 = {},
)
}
@@ -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"
)
@@ -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
@@ -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
)
}
@@ -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
)
*/
)
+11
View File
@@ -14,6 +14,11 @@ junit = "4.13.2"
kotlin = "2.3.21" kotlin = "2.3.21"
material3 = "1.10.0-alpha05" material3 = "1.10.0-alpha05"
koin-bom = "4.2.1" 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] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 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" } 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-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-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-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", 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-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-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", 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" } 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-ktor = { module = "io.insert-koin:koin-ktor" }
koin-test = { module = "io.insert-koin:koin-test" } 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] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }