diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d9..b589d56e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 639c779c..0897082f 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,7 +4,6 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 0bd3ec25..55c0ec2c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,6 @@ + - + diff --git a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt index 7f22e149..e9b490d8 100644 --- a/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt +++ b/app/src/main/java/com/greybox/projectmesh/GlobalApp.kt @@ -58,7 +58,7 @@ All dependencies are defined in one place, which makes it easier to manage and t class GlobalApp : Application(), DIAware { // it is an instance of Preferences.key, used to interact with "DataStore" private val addressKey = intPreferencesKey("virtual_node_address") - /*data object DeviceInfoManager { + data object DeviceInfoManager { // Global HashMap to store IP-DeviceName mapping private val deviceNameMap = ConcurrentHashMap() @@ -82,7 +82,7 @@ class GlobalApp : Application(), DIAware { fun getChatName(inetAddress: InetAddress): String { return inetAddress.hostAddress } - }*/ + } object GlobalUserRepo { // Lateinit or lazy property lateinit var userRepository: UserRepository @@ -365,7 +365,6 @@ class GlobalApp : Application(), DIAware { } }) .fallbackToDestructiveMigration() // handle migrations destructively -// .allowMainThreadQueries() // this should generally be avoided for production apps .build() } @@ -398,10 +397,6 @@ class GlobalApp : Application(), DIAware { } onReady { - // clears all data in the existing tables - //GlobalScope.launch { - // instance().messageDao().clearTable() - //} val logger: MNetLogger = instance() instance().start() logger(Log.DEBUG,"AppServer started successfully on Port: ${AppServer.DEFAULT_PORT}") diff --git a/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt b/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt index f08814f3..e2c2ea40 100644 --- a/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt +++ b/app/src/main/java/com/greybox/projectmesh/ui/theme/CustomButton.kt @@ -42,8 +42,8 @@ fun TransparentButton( contentColor = Color.Black // Text color ), border = BorderStroke(1.dp, Color.Black), // Black border - shape = RoundedCornerShape(8.dp), // Optional: Rounded corners - modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(vertical = 8.dp).size(50.dp), enabled = enabled ) { Text(text = text) diff --git a/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt b/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt index 3232e0b3..74b033f6 100644 --- a/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt +++ b/app/src/main/java/com/greybox/projectmesh/ui/theme/Theme.kt @@ -5,32 +5,34 @@ import androidx.compose.runtime.Composable import androidx.compose.material3.* import androidx.compose.ui.graphics.Color -// Define the color schemes for light and dark themes +enum class AppTheme { + SYSTEM, LIGHT, DARK +} + private val DarkColorScheme = darkColorScheme( - primary = Color(0xFFBB86FC), - secondary = Color(0xFF03DAC5), - background = Color(0xFF121212), - surface = Color(0xFF121212), + primary = Color(0xFFB6A4F5), onPrimary = Color.Black, - onSecondary = Color.Black, + background = Color(0xFF121212), onBackground = Color.White, - onSurface = Color.White + surface = Color(0xFF1E1E1E), + onSurface = Color.White, + secondary = Color(0xFFAAAAAA), + onSecondary = Color.Black, + outline = Color(0xFF444444) ) private val LightColorScheme = lightColorScheme( - primary = Color(0xFF6200EE), - secondary = Color(0xFF03DAC5), - background = Color(0xFFFFFFFF), - surface = Color(0xFFFFFFFF), + primary = Color(0xFF6A5AE0), // vibrant purple (used for buttons, etc.) onPrimary = Color.White, - onSecondary = Color.Black, + background = Color(0xFFF9F9F9), // soft white background onBackground = Color.Black, - onSurface = Color.Black + surface = Color.White, + onSurface = Color.Black, + secondary = Color(0xFFB0B0B0), // soft gray + onSecondary = Color.Black, + outline = Color(0xFFE0E0E0) // subtle border color ) -enum class AppTheme { - SYSTEM, LIGHT, DARK -} @Composable fun ProjectMeshTheme( @@ -42,9 +44,10 @@ fun ProjectMeshTheme( AppTheme.LIGHT -> false AppTheme.DARK -> true } + MaterialTheme( colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme, - content = content, typography = Typography, + content = content ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/greybox/projectmesh/ui/theme/Type.kt b/app/src/main/java/com/greybox/projectmesh/ui/theme/Type.kt index 5ae1fbb9..405daf65 100644 --- a/app/src/main/java/com/greybox/projectmesh/ui/theme/Type.kt +++ b/app/src/main/java/com/greybox/projectmesh/ui/theme/Type.kt @@ -15,20 +15,4 @@ val Typography = Typography( 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/app/src/main/java/com/greybox/projectmesh/views/HomeScreen.kt b/app/src/main/java/com/greybox/projectmesh/views/HomeScreen.kt index e7201360..f45e856d 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/HomeScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/views/HomeScreen.kt @@ -11,7 +11,7 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,15 +25,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -82,6 +87,9 @@ import org.kodein.di.compose.localDI import org.kodein.di.direct import org.kodein.di.instance import androidx.compose.runtime.State +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import com.greybox.projectmesh.extension.hasStaApConcurrency import com.greybox.projectmesh.ui.theme.TransparentButton import com.greybox.projectmesh.viewModel.HomeScreenModel @@ -210,9 +218,16 @@ fun StartHomeScreen( }, onResult = onConnectWifiLauncherResult, ) + val showSettings = remember { mutableStateOf(false) } var userEnteredConnectUri by rememberSaveable { mutableStateOf("") } + var showShareBox by remember { mutableStateOf(false) } + var showEnterUriBox by remember { mutableStateOf(false) } val showNoConcurrencyWarning by viewModel.showNoConcurrencyWarning.collectAsState() val showConcurrencyWarning by viewModel.showConcurrencyWarning.collectAsState() + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp + val screenHeightDp = configuration.screenHeightDp + // connect to other device via connect uri fun connect(uri: String, logger: MNetLogger): Unit { try { @@ -272,41 +287,60 @@ fun StartHomeScreen( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - item (key = "device_info") { - LongPressCopyableText( - context, - text = stringResource(id = R.string.device_name) + ": ", - textCopyable = deviceName.toString(), - textSize = 14, - padding = 6 - ) - LongPressCopyableText( - context, - text = stringResource(id = R.string.ip_address) + ": ", - textCopyable = uiState.localAddress.addressToDotNotation(), - textSize = 14, - padding = 6 - ) + item (key = "logo") { + // Logo & Title + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row { + Image(painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "logo", + modifier = Modifier.size(80.dp)) + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Text("Project", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text("MESH", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + } + } + IconButton( + onClick = { showSettings.value = true }, + modifier = Modifier + .padding(20.dp) + .size(60.dp)) { + Icon(Icons.Default.AccountCircle, + contentDescription = "Settings", + modifier = Modifier.fillMaxSize()) + } + } } item (key = "band_option"){ if (uiState.connectBandVisible) { - Text( - modifier = Modifier.padding(horizontal = 6.dp), - text = stringResource(id = R.string.band), - style = MaterialTheme.typography.bodySmall, - ) Row (modifier = Modifier.padding(horizontal = 6.dp)){ uiState.bandMenu.forEach { band -> FilterChip( selected = uiState.band == band, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp), + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 8.dp) + .size(100.dp, 50.dp), onClick = { onSetBand(band) }, label = { - Text(band.toString()) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(band.toString()) + } }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Color(0xFFE5E5E5), + selectedLabelColor = Color.Black, + containerColor = Color.White, + labelColor = Color.Black + ) ) } } @@ -316,20 +350,31 @@ fun StartHomeScreen( item (key = "hotspot_type_option") { if(!uiState.wifiConnectionEnabled) { val wifiDirectSupported = isWifiDirectSupported(context) - Text( - modifier = Modifier.padding(horizontal = 6.dp), - text = stringResource(id = R.string.hotspot_type), - style = MaterialTheme.typography.bodySmall, - ) Row(modifier = Modifier.padding(horizontal = 6.dp)){ uiState.hotspotTypeMenu.forEach { hotspotType -> val isDisabled = (hotspotType == HotspotType.WIFIDIRECT_GROUP && !wifiDirectSupported) FilterChip( enabled = !isDisabled, selected = hotspotType == uiState.hotspotTypeToCreate, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp), + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 8.dp) + .size(100.dp, 50.dp), onClick = { onSetHotspotTypeToCreate(hotspotType) }, - label = { Text(hotspotType.toString()) } + label = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(hotspotType.toString()) + } + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = Color(0xFFEAEAEA), + selectedLabelColor = Color.Black, + containerColor = Color.White, + labelColor = Color.Black + ) + ) } } @@ -343,7 +388,6 @@ fun StartHomeScreen( Row{ TransparentButton( onClick = { onSetIncomingConnectionsEnabled(true) }, - modifier = Modifier.padding(4.dp), text = stringResource(id = R.string.start_hotspot), // If not connected to a WiFi, enable the button // Else, check if the device supports WiFi STA/AP Concurrency @@ -366,7 +410,6 @@ fun StartHomeScreen( } } }, - modifier = Modifier.padding(4.dp), text = stringResource(id = R.string.stop_hotspot), enabled = true ) @@ -381,24 +424,38 @@ fun StartHomeScreen( QRCodeView( connectUri, barcodeEncoder, + showShareBox, + onToggleShareBox = { showShareBox = !showShareBox }, uiState.wifiState?.connectConfig?.ssid, uiState.wifiState?.connectConfig?.passphrase, uiState.wifiState?.connectConfig?.bssid, uiState.wifiState?.connectConfig?.port.toString() ) + // Display connectUri - Spacer(modifier = Modifier.height(16.dp)) - Text(text = stringResource(id = R.string.instruction_start_hotspot)) - Button(onClick = { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, connectUri) - type = "text/plain" + if(showShareBox){ + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = stringResource(id = R.string.instruction_start_hotspot), + modifier = Modifier.padding(start = 16.dp)) + TransparentButton( + onClick = { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, connectUri) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + }, + text = stringResource(id = R.string.share_connect_uri), + enabled = true, + ) + } } - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) - }, modifier = Modifier.padding(4.dp), enabled = true) { - Text(stringResource(id = R.string.share_connect_uri)) } } } @@ -410,15 +467,12 @@ fun StartHomeScreen( // It will display the option to connect via a QR code scan. if (stationState != null){ if (stationState.status == WifiStationState.Status.INACTIVE){ - Text(modifier = Modifier.padding(6.dp), text = stringResource(id = R.string.wifi_station_connection), style = TextStyle(fontSize = 16.sp)) - Spacer(modifier = Modifier.height(12.dp)) Row{ TransparentButton(onClick = { qrScannerLauncher.launch(ScanOptions().setOrientationLocked(false) .setPrompt("Scan another device to join the Mesh") .setBeepEnabled(true) )}, - modifier = Modifier.padding(4.dp), text = stringResource(id = R.string.connect_via_qr_code_scan), // If the hotspot isn't started, enable the button // Else, check if the device supports WiFi STA/AP Concurrency @@ -429,28 +483,52 @@ fun StartHomeScreen( currConcurrencySupported.value ) } - Text(modifier = Modifier.padding(6.dp), text = stringResource(id = R.string.instruction)) - TextField( - value = userEnteredConnectUri, - onValueChange = { - userEnteredConnectUri = it - }, - label = { Text(stringResource(id = R.string.prompt_enter_uri)) } - ) - TransparentButton( - onClick = { - connect(userEnteredConnectUri, logger) - }, - modifier = Modifier.padding(4.dp), - text = stringResource(id = R.string.connect_via_entering_connect_uri), - // If the hotspot isn't started, enable the button - // Else, check if the device supports WiFi STA/AP Concurrency - // If it does, enable the button. Otherwise, disable it - enabled = if (!uiState.hotspotStatus) - true - else - currConcurrencySupported.value - ) + Column(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + Row( + modifier = Modifier + .clickable { showEnterUriBox = !showEnterUriBox } + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Manual Entry", + textDecoration = TextDecoration.Underline, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = Icons.AutoMirrored.Filled.Help, + contentDescription = "QuestionMark", + modifier = Modifier.size(20.dp) + ) + } + } + + if (showEnterUriBox) { + Column{ + Text( + modifier = Modifier.padding(start = 16.dp, top = 6.dp, bottom = 6.dp), + text = stringResource(id = R.string.instruction) + ) + TextField( + value = userEnteredConnectUri, + onValueChange = { userEnteredConnectUri = it }, + label = { Text(stringResource(id = R.string.prompt_enter_uri)) }, + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + ) + TransparentButton( + onClick = { connect(userEnteredConnectUri, logger) }, + modifier = Modifier.padding(vertical = 4.dp), + text = stringResource(id = R.string.connect_via_entering_connect_uri), + enabled = !uiState.hotspotStatus || currConcurrencySupported.value + ) + } + } + } } // If the stationState is not INACTIVE, it displays a ListItem that represents // the current connection status. @@ -472,19 +550,24 @@ fun StartHomeScreen( ) }else { Icon( + modifier = Modifier.padding(6.dp), imageVector = Icons.Default.Wifi, - contentDescription = "", + contentDescription = "Wifi Icon", ) } }, trailingContent = { IconButton( onClick = { - onClickDisconnectWifiStation() + disconnectConfirmationDialog(context) { onConfirm -> + if (onConfirm) { + onClickDisconnectWifiStation() + } + } } ) { Icon( - imageVector = Icons.Default.Close, + imageVector = Icons.Default.Cancel, contentDescription = "Disconnect", ) } @@ -494,20 +577,71 @@ fun StartHomeScreen( } } - // add a Hotspot status indicator - item(key = "hotspot_status_indicator"){ - Row(verticalAlignment = Alignment.CenterVertically) { - Text(modifier = Modifier.padding(6.dp), - text = stringResource(id = R.string.hotspot_status) + ": " + - if (uiState.hotspotStatus) stringResource( - id = R.string.hotspot_status_online) - else stringResource(id = R.string.hotspot_status_offline)) - Box( - modifier = Modifier.size(8.dp).background( - color = if (uiState.hotspotStatus) Color.Green else Color.Red, - shape = CircleShape + // add a Mesh status indicator + item(key = "mesh_status_indicator"){ + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Mesh Status", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) ) - ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + if (uiState.nodesOnMesh.isNotEmpty()){ + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Online", + tint = Color(0xFF4CAF50), // Green color + modifier = Modifier.size(28.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "Online", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = "No. of Nodes: " + uiState.nodesOnMesh.size, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) + } + } + else{ + Icon( + imageVector = Icons.Default.Cancel, + contentDescription = "Offline", + tint = Color.Red, // Green color + modifier = Modifier.size(28.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "Offline", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = "No. of Nodes: 0", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) + } + } + } + } } } } @@ -525,6 +659,18 @@ fun stopHotspotConfirmationDialog(context: Context, onConfirm: (Boolean) -> Unit .show() } +fun disconnectConfirmationDialog(context: Context, onConfirm: (Boolean) -> Unit){ + AlertDialog.Builder(context) + .setTitle("Do you want to disconnect the network?") + .setPositiveButton("Yes"){ _, _ -> + onConfirm(true) + } + .setNegativeButton("No"){ _, _ -> + onConfirm(false) + } + .show() +} + // Enable users to copy text by holding down the text for a long press @Composable fun LongPressCopyableText(context: Context, @@ -554,38 +700,48 @@ fun LongPressCopyableText(context: Context, // display the QR code @Composable -fun QRCodeView(qrcodeUri: String, barcodeEncoder: BarcodeEncoder, ssid: String?, password: String?, +fun QRCodeView(qrcodeUri: String, barcodeEncoder: BarcodeEncoder, + showShareBox: Boolean, onToggleShareBox: () -> Unit, + ssid: String?, password: String?, mac: String?, port: String?) { val configuration = LocalConfiguration.current val screenWidthDp = configuration.screenWidthDp.dp val density = LocalDensity.current // Convert dp to int once and remember the value val qrCodeSize = remember(density, screenWidthDp) { - with(density) { screenWidthDp.times(0.35f).roundToPx() } // Converts to Int + with(density) { screenWidthDp.times(0.5f).roundToPx() } // Converts to Int } val qrCodeBitMap = remember(qrcodeUri) { barcodeEncoder.encodeBitmap( qrcodeUri, BarcodeFormat.QR_CODE, qrCodeSize, qrCodeSize ).asImageBitmap() } - Row (modifier = Modifier.fillMaxWidth()) { - // QR Code left side, Device info on the right side - Image( - bitmap = qrCodeBitMap, - contentDescription = "QR Code" - ) - Spacer(modifier = Modifier.width(8.dp)) - Column( - modifier = Modifier.weight(1f) - ) { - Spacer(modifier = Modifier.height(10.dp)) - Text(text = "SSID: $ssid") - Spacer(modifier = Modifier.height(10.dp)) - Text(text = "Password: $password") - Spacer(modifier = Modifier.height(10.dp)) - Text(text = "MAC: $mac") - Spacer(modifier = Modifier.height(10.dp)) - Text(text = "Port: $port") + Box (modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "Scan QR Code To Connect", + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold), + modifier = Modifier.padding(bottom = 16.dp) + ) + Image( + bitmap = qrCodeBitMap, + contentDescription = "QR Code" + ) + Spacer(modifier = Modifier.height(8.dp)) + Row ( + modifier = Modifier.clickable{ onToggleShareBox() } + ){ + Text(text = "Share Connect URI ", textDecoration = TextDecoration.Underline) + Icon( + imageVector = Icons.AutoMirrored.Filled.Help, + contentDescription = "QuestionMark", + modifier = Modifier.size(24.dp) + ) + } } } } diff --git a/app/src/main/java/com/greybox/projectmesh/views/LogScreen.kt b/app/src/main/java/com/greybox/projectmesh/views/LogScreen.kt index df0ed1c2..23b7808a 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/LogScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/views/LogScreen.kt @@ -1,13 +1,10 @@ package com.greybox.projectmesh.views import android.widget.Toast -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -30,7 +27,6 @@ import androidx.compose.runtime.setValue 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.input.pointer.pointerInput import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext diff --git a/app/src/main/java/com/greybox/projectmesh/views/ReceiveScreen.kt b/app/src/main/java/com/greybox/projectmesh/views/ReceiveScreen.kt index 6fb7712c..ec4c6df2 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/ReceiveScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/views/ReceiveScreen.kt @@ -19,17 +19,20 @@ 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.width +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -42,11 +45,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSavedStateRegistryOwner -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.lifecycle.viewmodel.compose.viewModel -import com.greybox.projectmesh.R import com.greybox.projectmesh.ViewModelFactory import com.greybox.projectmesh.server.AppServer import com.greybox.projectmesh.viewModel.ReceiveScreenViewModel @@ -54,7 +55,9 @@ import org.kodein.di.compose.localDI import org.kodein.di.DI import org.kodein.di.instance import java.io.File -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color import com.greybox.projectmesh.viewModel.ReceiveScreenModel @Composable @@ -146,71 +149,79 @@ fun HandleIncomingTransfers( } } } - LazyColumn(modifier = Modifier.fillMaxSize()) { - items( - items = uiState.incomingTransfers, - key = {"${it.fromHost.hostAddress}-${it.id}-${it.requestReceivedTime}".hashCode()} - ){ transfer -> - ListItem( + LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) { + items(uiState.incomingTransfers, key = { + "${it.fromHost.hostAddress}-${it.id}-${it.requestReceivedTime}".hashCode() + }) { transfer -> + val progress = if (transfer.size == 0) 0f else transfer.transferred / transfer.size.toFloat() + val isCompleted = transfer.status == AppServer.Status.COMPLETED + val isFailedOrDeclined = transfer.status == AppServer.Status.DECLINED || transfer.status == AppServer.Status.FAILED + + Card( + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(6.dp), modifier = Modifier - .clickable { + .fillMaxWidth() + .padding(vertical = 6.dp) + .clickable(enabled = isCompleted) { openFile(transfer) } - .fillMaxWidth(), - headlineContent = { - Text(transfer.name) - }, - supportingContent = { - Column{ - val fromHostAddress = transfer.fromHost.hostAddress - Text(stringResource(id = R.string.from) + ":") - Text("${transfer.deviceName}(${fromHostAddress})") - Text(stringResource(id = R.string.status) + ": ${transfer.status}") - Text(autoConvertByte(transfer.transferred) + "/" + autoConvertByte(transfer.size)) - if(transfer.status == AppServer.Status.COMPLETED){ - Text(stringResource(id = R.string.elapsed_time) + ": ${autoConvertMS(transfer.transferTime)}") - } - if(transfer.status == AppServer.Status.PENDING){ - Row{ - IconButton(onClick = {onAccept(transfer)}, - modifier = Modifier.width(100.dp)) { + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(transfer.name, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(4.dp)) + Text("From: ${transfer.deviceName} (${transfer.fromHost.hostAddress})", style = MaterialTheme.typography.bodySmall) + Text("Status: ${transfer.status}", style = MaterialTheme.typography.bodySmall) + + Spacer(modifier = Modifier.height(6.dp)) + + if (transfer.status != AppServer.Status.PENDING) { + LinearProgressIndicator( + progress = { progress }, + color = if (isCompleted) Color(0xFF4CAF50) else MaterialTheme.colorScheme.primary, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${autoConvertByte(transfer.transferred)} / ${autoConvertByte(transfer.size)}", + style = MaterialTheme.typography.labelSmall + ) + } + + if (isCompleted) { + Text("Elapsed Time: ${autoConvertMS(transfer.transferTime)}", style = MaterialTheme.typography.labelSmall) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + when { + transfer.status == AppServer.Status.PENDING -> { + IconButton(onClick = { onAccept(transfer) }) { Icon(Icons.Default.CheckCircle, contentDescription = "Accept") } - Spacer(modifier = Modifier.width(8.dp)) - IconButton(onClick = {onDecline(transfer)}, - modifier = Modifier.width(100.dp)) { + IconButton(onClick = { onDecline(transfer) }) { Icon(Icons.Default.Cancel, contentDescription = "Decline") } } - } - } - }, - trailingContent = { - if(transfer.status == AppServer.Status.COMPLETED){ - Row ( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) // Space between buttons - ){ - IconButton(onClick = {onDelete(transfer)}) { - Icon(Icons.Default.Delete, contentDescription = "Delete") - } - IconButton(onClick = {onDownload(context, transfer, defaultUri)}) { - Icon(Icons.Default.Download, contentDescription = "Download") + isCompleted -> { + IconButton(onClick = { onDelete(transfer) }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + IconButton(onClick = { onDownload(context, transfer, defaultUri) }) { + Icon(Icons.Default.Download, contentDescription = "Download") + } } - } - } - else if(transfer.status == AppServer.Status.DECLINED || transfer.status == AppServer.Status.FAILED){ - Row ( - verticalAlignment = Alignment.CenterVertically - ){ - IconButton(onClick = {onDelete(transfer)}) { - Icon(Icons.Default.Delete, contentDescription = "Delete") + isFailedOrDeclined -> { + IconButton(onClick = { onDelete(transfer) }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } } } } } - ) - HorizontalDivider() + } } } } @@ -338,4 +349,5 @@ private fun saveFileToContentUri(context: Context, transfer: AppServer.IncomingT e.printStackTrace() Toast.makeText(context, "Error saving file: ${e.localizedMessage}", Toast.LENGTH_LONG).show() } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/greybox/projectmesh/views/SendScreen.kt b/app/src/main/java/com/greybox/projectmesh/views/SendScreen.kt index 7293d1a5..eba02db4 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/SendScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/views/SendScreen.kt @@ -4,36 +4,42 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border 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.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.DismissValue +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.rememberSwipeToDismissBoxState 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 @@ -42,21 +48,16 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSavedStateRegistryOwner -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.greybox.projectmesh.GlobalApp -import com.greybox.projectmesh.R import com.greybox.projectmesh.ViewModelFactory -import com.greybox.projectmesh.ui.theme.TransparentButton import com.greybox.projectmesh.viewModel.SendScreenModel import com.greybox.projectmesh.viewModel.SendScreenViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.kodein.di.compose.localDI @Composable @@ -81,107 +82,134 @@ fun SendScreen( viewModel.onFileChosen(uris) } } - Box(modifier = Modifier.fillMaxSize()){ - Column(modifier = Modifier - .fillMaxSize() - .padding(bottom = 72.dp)) { - DisplayAllPendingTransfers(viewModel, uiState) + Scaffold( + floatingActionButton = { + FloatingActionButton(onClick = { openDocumentLauncher.launch(arrayOf("*/*")) }) { + Icon(Icons.Default.Add, contentDescription = "Pick Files") + } } - TransparentButton(onClick = { openDocumentLauncher.launch(arrayOf("*/*")) }, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(16.dp), - text = stringResource(id = R.string.send_file), - enabled = true + ) { innerPadding -> + DisplayAllPendingTransfers( + viewModel = viewModel, + uiState = uiState, + modifier = Modifier.padding(innerPadding) ) } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -// Display all the pending transfers fun DisplayAllPendingTransfers( viewModel: SendScreenViewModel, uiState: SendScreenModel, -){ + modifier: Modifier = Modifier +) { val coroutineScope = rememberCoroutineScope() - LazyColumn { - items( - items = uiState.outgoingTransfers, - key = { it.id } - ) {transfer -> + LazyColumn(modifier = modifier.padding(16.dp)) { + items(uiState.outgoingTransfers, key = { it.id }) { transfer -> var isVisible by remember { mutableStateOf(true) } val swipeState = rememberSwipeToDismissBoxState( confirmValueChange = { dismissValue -> if (dismissValue == SwipeToDismissBoxValue.EndToStart) { coroutineScope.launch { - isVisible = false // Start fade-out animation + isVisible = false delay(300) - viewModel.onDelete(transfer) // Remove item on swipe + viewModel.onDelete(transfer) } true - } else { - false - } + } else false } ) AnimatedVisibility( visible = isVisible, - exit = fadeOut(animationSpec = tween(300)), + exit = fadeOut(tween(300)), modifier = Modifier.animateItemPlacement() - ){ + ) { SwipeToDismissBox( state = swipeState, - enableDismissFromStartToEnd = false, // Allow swipe only from right to left + enableDismissFromStartToEnd = false, enableDismissFromEndToStart = true, backgroundContent = { Box( modifier = Modifier .fillMaxSize() - .height(64.dp) // Controls the red background size - .padding(vertical = 8.dp) // Prevents red from touching top & bottom - .background(Color.Red, shape = RoundedCornerShape(12.dp)) - .border(width = 0.dp, color = Color.Transparent, shape = RoundedCornerShape(12.dp)), + .height(96.dp) + .padding(vertical = 8.dp) + .background(Color.Red, shape = RoundedCornerShape(16.dp)), contentAlignment = Alignment.CenterEnd ) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Delete", tint = Color.White, - modifier = Modifier.size(32.dp).padding(end = 6.dp) + modifier = Modifier + .padding(end = 20.dp) + .size(32.dp) ) } }, content = { - ListItem( - headlineContent = { Text(transfer.name) }, - supportingContent = { - Column { - val byteTransferred: Int = transfer.transferred - val byteSize: Int = transfer.size - val toHostAddress = transfer.toHost.hostAddress - val deviceName = toHostAddress?.let { ipStr -> - runBlocking { - GlobalApp.GlobalUserRepo.userRepository.getUserByIp(ipStr)?.name - } - } -// val deviceName = toHostAddress?.let { -// GlobalApp.DeviceInfoManager.getDeviceName(it) -// } - if (deviceName != null) { - Text("To: ${deviceName} (${toHostAddress})") - } else { - Text("To: Loading... (${toHostAddress})") + val byteTransferred = transfer.transferred + val byteSize = transfer.size + val progress = if (byteSize == 0) 0f else byteTransferred / byteSize.toFloat() + val deviceName = GlobalApp.DeviceInfoManager.getDeviceName(transfer.toHost.hostAddress ?: "") + + Card( + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(6.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(text = transfer.name, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "To: ${deviceName ?: "Loading..."} (${transfer.toHost.hostAddress})", + style = MaterialTheme.typography.bodySmall + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Status: ${transfer.status}", + style = MaterialTheme.typography.bodySmall + ) + Spacer(Modifier.width(8.dp)) + when (transfer.status.toString()) { + "COMPLETED" -> Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Completed", + tint = Color(0xFF4CAF50), + modifier = Modifier.size(16.dp) + ) + "IN_PROGRESS" -> Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = "In Progress", + tint = Color(0xFF2196F3), + modifier = Modifier.size(16.dp) + ) + "FAILED", "DECLINED" -> Icon( + imageVector = Icons.Default.Cancel, + contentDescription = "Failed", + tint = Color.Red, + modifier = Modifier.size(16.dp) + ) } - Text(stringResource(id = R.string.status) + ": ${transfer.status}") - Text(stringResource(id = R.string.send) + ": ${autoConvertByte(byteTransferred)} / ${autoConvertByte(byteSize)}") } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) - }, + Spacer(modifier = Modifier.height(6.dp)) + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth(), + color = if (transfer.status.toString() == "COMPLETED") Color(0xFF4CAF50) else MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${autoConvertByte(byteTransferred)} / ${autoConvertByte(byteSize)}", + style = MaterialTheme.typography.labelSmall + ) + } + } + } ) - HorizontalDivider() } } } diff --git a/app/src/main/java/com/greybox/projectmesh/views/SettingsScreen.kt b/app/src/main/java/com/greybox/projectmesh/views/SettingsScreen.kt index 2ae8e9d2..1ca4c0ed 100644 --- a/app/src/main/java/com/greybox/projectmesh/views/SettingsScreen.kt +++ b/app/src/main/java/com/greybox/projectmesh/views/SettingsScreen.kt @@ -2,7 +2,6 @@ package com.greybox.projectmesh.views import android.content.Intent import android.content.SharedPreferences -import android.graphics.Paint.Align import android.net.Uri import android.os.Build import android.widget.Toast @@ -17,12 +16,20 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DarkMode +import androidx.compose.material.icons.filled.DoneAll +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Restore import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -40,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSavedStateRegistryOwner import androidx.compose.ui.res.stringResource @@ -54,9 +62,6 @@ import com.greybox.projectmesh.R import com.greybox.projectmesh.ViewModelFactory import com.greybox.projectmesh.ui.theme.AppTheme import com.greybox.projectmesh.ui.theme.GradientButton -import com.greybox.projectmesh.ui.theme.GradientLongButton -import com.greybox.projectmesh.viewModel.HomeScreenViewModel -import com.greybox.projectmesh.viewModel.SendScreenViewModel import com.greybox.projectmesh.viewModel.SettingsScreenViewModel import org.kodein.di.compose.localDI import org.kodein.di.instance @@ -90,20 +95,17 @@ fun SettingsScreen( val settingPref: SharedPreferences by di.instance(tag = "settings") - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) + Column(modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState())) { - Spacer(modifier = Modifier.height(36.dp)) - // Title "Settings" - Text( - text = stringResource(id = R.string.settings), - style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Column(modifier = Modifier.padding(36.dp)) { + Column(modifier = Modifier.padding(6.dp)) { // General Setting Section (Language, Theme) SectionHeader(title = R.string.general) SettingItem( label = stringResource(id = R.string.language), + icon = Icons.Default.Language, + contentDescription = "Language", trailingContent = { LanguageSetting( currentLanguage = currLang.value, @@ -116,6 +118,8 @@ fun SettingsScreen( ) SettingItem( label = stringResource(id = R.string.theme), + icon = Icons.Default.DarkMode, + contentDescription = "Theme", trailingContent = { ThemeSetting( currentTheme = currTheme.value, @@ -130,6 +134,8 @@ fun SettingsScreen( SectionHeader(title = R.string.network) SettingItem( label = stringResource(id = R.string.server), + icon = Icons.Default.Refresh, + contentDescription = "Restart Server", trailingContent = { GradientButton( text = stringResource(id = R.string.restart), @@ -139,6 +145,8 @@ fun SettingsScreen( ) SettingItem( label = stringResource(id = R.string.device_name), + icon = Icons.Default.Edit, + contentDescription = "Edit Device Name", trailingContent = { GradientButton(text = currDeviceName.value, onClick = { showDialog = true }) } @@ -157,8 +165,12 @@ fun SettingsScreen( // Receive Setting Section (Auto Finish, Save to folder) SectionHeader(title = R.string.receive) SettingItem(label = stringResource(id = R.string.auto_finish), + icon = Icons.Default.DoneAll, + contentDescription = "Auto Finish", trailingContent = { - Box(modifier = Modifier.width(130.dp).height(70.dp)) + Box(modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.CenterEnd) { Switch( checked = currAutoFinish.value, @@ -172,7 +184,7 @@ fun SettingsScreen( checkedTrackColor = Color(0xFF4CAF50), uncheckedTrackColor = Color.LightGray, ), - modifier = Modifier.scale(1.3f).align(Alignment.Center) + modifier = Modifier.scale(0.9f).padding(0.dp,0.dp,16.dp, 0.dp) ) } } @@ -201,6 +213,8 @@ fun SettingsScreen( currSaveToFolder.value.split("/").lastOrNull() ?: "Unknown" } SettingItem(label = stringResource(id = R.string.save_to_folder), + icon = Icons.Default.Folder, + contentDescription = "Save to folder", trailingContent = { GradientButton(text = folderNameToShow, onClick = { directoryLauncher.launch(null) } @@ -210,9 +224,11 @@ fun SettingsScreen( // STA/AP Concurrency Setting Section (Only for Android 10 and below) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { SectionHeader(title = R.string.concurrency) - SettingItem(label = "", + SettingItem(label = "Concurrency", + icon = Icons.Default.Restore, + contentDescription = "Sta ap concurrency", trailingContent = { - GradientLongButton( + GradientButton( text = stringResource(id = R.string.reset), onClick = { viewModel.updateConcurrencySettings(false, true) @@ -231,29 +247,37 @@ fun SettingsScreen( } @Composable fun SectionHeader(title: Int) { - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(18.dp)) Text( text = stringResource(id = title), - style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold) + style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold) ) HorizontalDivider( modifier = Modifier .fillMaxWidth() .padding(0.dp, 10.dp), thickness = 2.dp, - color = Color.Red + color = Color.LightGray ) } @Composable -fun SettingItem(label: String, trailingContent: @Composable () -> Unit) { +fun SettingItem(label: String, + icon: ImageVector, + contentDescription: String, + trailingContent: @Composable () -> Unit) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(text = label, style = TextStyle(fontSize = 18.sp)) + Icon( + imageVector = icon, + contentDescription = contentDescription, + modifier = Modifier.padding(end = 12.dp) + ) + Text(text = label, style = TextStyle(fontSize = 14.sp)) Spacer(modifier = Modifier.weight(1f)) trailingContent() } @@ -267,9 +291,7 @@ fun LanguageSetting( // for Language setting var langExpanded by remember { mutableStateOf(false) } // Track menu visibility val langMenuItems = listOf("en" to "English", "es" to "Español", "cn" to "简体中文", "fr" to - "Français") // - // Menu - // items + "Français") // Menu items val langSelectedOption = langMenuItems.firstOrNull {it.first == currentLanguage}?.second?:"English" Box() { diff --git a/app/src/main/res/values-fr-rCA/strings.xml b/app/src/main/res/values-fr-rCA/strings.xml index 1270e152..cea1d5f9 100644 --- a/app/src/main/res/values-fr-rCA/strings.xml +++ b/app/src/main/res/values-fr-rCA/strings.xml @@ -60,4 +60,6 @@ Concurrence STA/AP Test manuel Réinitialiser + + Journal \ No newline at end of file