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