21d8f0dca2
App icon: deep indigo-to-violet gradient background with white sync arrows; replaced flat #2196F3 with layered adaptive icon. Theme: disabled dynamic color; rich indigo/teal/amber Material3 palette; edge-to-edge with transparent status bar; tighter typography letterSpacing. HomeScreen: colored left accent bar per status; URL-decoded SAF paths; relative timestamps (Just now / N min ago / N hr ago); indigo status pills; FilledTonalButton empty state. PairDetailScreen: hero StatusBanner with large icon and relative time; InfoCard as bordered grid with icon backgrounds; colored dot event timeline; URL-decoded local path display. SettingsScreen: section headers with primary left bar; AccountCard with primaryContainer icon backgrounds; Security/About in bordered cards. Bump version to 1.0.13 (code 14). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
241 lines
9.6 KiB
Kotlin
241 lines
9.6 KiB
Kotlin
package com.syncflow.ui.settings
|
|
|
|
import androidx.compose.foundation.BorderStroke
|
|
import androidx.compose.foundation.layout.*
|
|
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.*
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
import com.syncflow.data.db.entities.CloudAccountEntity
|
|
import com.syncflow.domain.model.ProviderType
|
|
|
|
@Composable
|
|
fun SettingsScreen(
|
|
onAddAccount: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
vm: SettingsViewModel = hiltViewModel(),
|
|
) {
|
|
val accounts by vm.accounts.collectAsState()
|
|
val biometricEnabled by vm.biometricEnabled.collectAsState()
|
|
var deleteTarget by remember { mutableStateOf<CloudAccountEntity?>(null) }
|
|
|
|
deleteTarget?.let { acct ->
|
|
AlertDialog(
|
|
onDismissRequest = { deleteTarget = null },
|
|
title = { Text("Remove account?") },
|
|
text = { Text("\"${acct.displayName}\" and all associated sync pairs will be removed.") },
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = { vm.removeAccount(acct); deleteTarget = null },
|
|
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
|
) { Text("Remove") }
|
|
},
|
|
dismissButton = { TextButton(onClick = { deleteTarget = null }) { Text("Cancel") } },
|
|
)
|
|
}
|
|
|
|
LazyColumn(
|
|
modifier = modifier.fillMaxSize(),
|
|
contentPadding = PaddingValues(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
) {
|
|
item {
|
|
SectionHeader(title = "Cloud Accounts")
|
|
Spacer(Modifier.height(4.dp))
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.End,
|
|
) {
|
|
FilledTonalButton(onClick = onAddAccount) {
|
|
Icon(Icons.Default.Add, null, Modifier.size(18.dp))
|
|
Spacer(Modifier.width(6.dp))
|
|
Text("Add Account")
|
|
}
|
|
}
|
|
}
|
|
|
|
if (accounts.isEmpty()) {
|
|
item {
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
) {
|
|
Icon(
|
|
Icons.Default.CloudOff,
|
|
null,
|
|
Modifier.size(48.dp),
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
|
)
|
|
Text(
|
|
"No accounts yet",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
Text(
|
|
"Add a cloud account to start syncing",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
OutlinedButton(onClick = onAddAccount) {
|
|
Icon(Icons.Default.Add, null, Modifier.size(16.dp))
|
|
Spacer(Modifier.width(6.dp))
|
|
Text("Add your first account")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
items(accounts, key = { it.id }) { acct ->
|
|
AccountCard(acct = acct, onDelete = { deleteTarget = acct })
|
|
}
|
|
}
|
|
|
|
item {
|
|
Spacer(Modifier.height(8.dp))
|
|
SectionHeader(title = "Security")
|
|
Spacer(Modifier.height(4.dp))
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(16.dp),
|
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Text("Biometric Lock", style = MaterialTheme.typography.bodyMedium)
|
|
Text(
|
|
"Require biometrics when returning to app",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
}
|
|
Switch(checked = biometricEnabled, onCheckedChange = { vm.setBiometricLock(it) })
|
|
}
|
|
}
|
|
}
|
|
|
|
item {
|
|
Spacer(Modifier.height(8.dp))
|
|
SectionHeader(title = "About")
|
|
Spacer(Modifier.height(4.dp))
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(16.dp),
|
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
Text(
|
|
"SyncFlow v${com.syncflow.BuildConfig.VERSION_NAME} — Free, no subscription.",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
)
|
|
Text(
|
|
"Open source. No ads. No tracking.",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SectionHeader(title: String) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Box(
|
|
modifier = Modifier
|
|
.width(3.dp)
|
|
.height(16.dp)
|
|
.clip(RoundedCornerShape(2.dp)),
|
|
) {
|
|
Surface(color = MaterialTheme.colorScheme.primary, modifier = Modifier.fillMaxSize()) {}
|
|
}
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
title,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = MaterialTheme.colorScheme.onBackground,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun AccountCard(acct: CloudAccountEntity, onDelete: () -> Unit) {
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(16.dp),
|
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
|
) {
|
|
Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
// Icon with primaryContainer background
|
|
Surface(
|
|
shape = RoundedCornerShape(12.dp),
|
|
color = MaterialTheme.colorScheme.primaryContainer,
|
|
modifier = Modifier.size(40.dp),
|
|
) {
|
|
Box(contentAlignment = Alignment.Center) {
|
|
Icon(
|
|
providerIcon(acct.providerType),
|
|
null,
|
|
Modifier.size(22.dp),
|
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
)
|
|
}
|
|
}
|
|
Spacer(Modifier.width(12.dp))
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Text(acct.displayName, style = MaterialTheme.typography.bodyMedium)
|
|
Text(
|
|
buildString {
|
|
append(friendlyProviderName(acct.providerType))
|
|
acct.email?.let { append(" · $it") }
|
|
},
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
acct.serverUrl?.let {
|
|
Text(it, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
}
|
|
}
|
|
IconButton(onClick = onDelete) {
|
|
Icon(Icons.Default.Delete, "Remove", tint = MaterialTheme.colorScheme.error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun providerIcon(type: ProviderType) = when (type) {
|
|
ProviderType.GOOGLE_DRIVE -> Icons.Default.Cloud
|
|
ProviderType.DROPBOX -> Icons.Default.CloudQueue
|
|
ProviderType.ONEDRIVE -> Icons.Default.CloudDone
|
|
ProviderType.WEBDAV -> Icons.Default.Storage
|
|
ProviderType.SFTP -> Icons.Default.Terminal
|
|
ProviderType.NEXTCLOUD -> Icons.Default.CloudCircle
|
|
ProviderType.OWNCLOUD -> Icons.Default.CloudCircle
|
|
ProviderType.SFTPGO -> Icons.Default.Storage
|
|
}
|
|
|
|
private fun friendlyProviderName(type: ProviderType) = when (type) {
|
|
ProviderType.GOOGLE_DRIVE -> "Google Drive"
|
|
ProviderType.DROPBOX -> "Dropbox"
|
|
ProviderType.ONEDRIVE -> "OneDrive"
|
|
ProviderType.WEBDAV -> "WebDAV"
|
|
ProviderType.SFTP -> "SFTP"
|
|
ProviderType.NEXTCLOUD -> "Nextcloud"
|
|
ProviderType.OWNCLOUD -> "ownCloud"
|
|
ProviderType.SFTPGO -> "SFTPGo"
|
|
}
|