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:
2026-05-22 20:21:20 +00:00
commit cff4233de6
95 changed files with 5381 additions and 0 deletions
@@ -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) }
}
}