Initial commit — SyncFlow Android file sync app
Supports WebDAV, SFTP, SFTPGo, Nextcloud, ownCloud, Google Drive, Dropbox, and OneDrive. Credentials encrypted with Android Keystore. Biometric app-lock, conflict resolution, and auto-sync via WorkManager. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package com.syncflow.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.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 {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Cloud Accounts", style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f))
|
||||
FilledTonalButton(onClick = onAddAccount) {
|
||||
Icon(Icons.Default.Add, null, Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("Add Account")
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
|
||||
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)
|
||||
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(16.dp))
|
||||
Text("Security", style = MaterialTheme.typography.titleMedium)
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.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(16.dp))
|
||||
Text("About", style = MaterialTheme.typography.titleMedium)
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Text("SyncFlow v1.0.0 — 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 AccountCard(acct: CloudAccountEntity, onDelete: () -> Unit) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(providerIcon(acct.providerType), null, Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.syncflow.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.syncflow.data.db.entities.CloudAccountEntity
|
||||
import com.syncflow.data.preferences.AppPreferences
|
||||
import com.syncflow.data.repository.AccountRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val appPreferences: AppPreferences,
|
||||
) : ViewModel() {
|
||||
|
||||
val accounts = accountRepository.observeAll()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
val biometricEnabled = appPreferences.biometricLockEnabled
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||
|
||||
fun setBiometricLock(enabled: Boolean) {
|
||||
viewModelScope.launch { appPreferences.setBiometricLock(enabled) }
|
||||
}
|
||||
|
||||
fun removeAccount(account: CloudAccountEntity) {
|
||||
viewModelScope.launch { accountRepository.delete(account) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user