redesign: modern indigo UI, new app icon, edge-to-edge theme

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>
This commit is contained in:
2026-05-24 02:31:44 +00:00
parent 3d7a8b5f3d
commit 21d8f0dca2
19 changed files with 589 additions and 191 deletions
@@ -1,9 +1,11 @@
package com.syncflow.ui.home package com.syncflow.ui.home
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
@@ -11,11 +13,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.data.db.entities.SyncPairEntity import com.syncflow.data.db.entities.SyncPairEntity
import com.syncflow.domain.model.SyncStatus import com.syncflow.domain.model.SyncStatus
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -58,49 +61,107 @@ private fun SyncPairCard(
onSync: () -> Unit, onSync: () -> Unit,
onToggle: () -> Unit, onToggle: () -> Unit,
) { ) {
ElevatedCard( val accentColor = pair.lastSyncResult.accentColor
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) { // Colored left accent bar
Column(modifier = Modifier.weight(1f)) { Box(
Text(pair.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) modifier = Modifier
Spacer(Modifier.height(2.dp)) .width(3.dp)
Text( .height(IntrinsicSize.Min)
pair.localPath.takeLast(40), .defaultMinSize(minHeight = 80.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(checked = pair.isEnabled, onCheckedChange = { onToggle() })
}
Spacer(Modifier.height(10.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
StatusChip(pair.lastSyncResult) Box(
if (pair.pendingConflicts > 0) { modifier = Modifier
AssistChip( .fillMaxHeight()
onClick = {}, .width(3.dp),
label = { Text("${pair.pendingConflicts} conflicts") }, ) {
leadingIcon = { Icon(Icons.Default.Warning, null, Modifier.size(16.dp)) }, Surface(
colors = AssistChipDefaults.assistChipColors( modifier = Modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.errorContainer, color = accentColor,
), ) {}
}
}
Column(modifier = Modifier.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
Text(
pair.name,
style = MaterialTheme.typography.titleMedium,
)
Spacer(Modifier.height(2.dp))
val localShortName = pair.localPath.toDisplayPath()
Text(
localShortName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(2.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.ArrowForward,
contentDescription = null,
modifier = Modifier.size(12.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.width(4.dp))
Text(
pair.remotePath,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Switch(
checked = pair.isEnabled,
onCheckedChange = { onToggle() },
) )
} }
Spacer(Modifier.weight(1f)) Spacer(Modifier.height(8.dp))
pair.lastSyncAt?.let { at -> Row(
Text( verticalAlignment = Alignment.CenterVertically,
at.formatRelative(), horizontalArrangement = Arrangement.spacedBy(8.dp),
style = MaterialTheme.typography.labelSmall, ) {
color = MaterialTheme.colorScheme.onSurfaceVariant, StatusPill(pair.lastSyncResult)
) if (pair.pendingConflicts > 0) {
} Surface(
FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) { shape = RoundedCornerShape(50),
Icon(Icons.Default.Sync, "Sync now", modifier = Modifier.size(18.dp)) color = MaterialTheme.colorScheme.errorContainer,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(Icons.Default.Warning, null, Modifier.size(12.dp), tint = MaterialTheme.colorScheme.error)
Text(
"${pair.pendingConflicts} conflicts",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error,
)
}
}
}
Spacer(Modifier.weight(1f))
pair.lastSyncAt?.let { at ->
Text(
at.toRelativeString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) {
Icon(Icons.Default.Sync, "Sync now", modifier = Modifier.size(18.dp))
}
} }
} }
} }
@@ -108,21 +169,33 @@ private fun SyncPairCard(
} }
@Composable @Composable
private fun StatusChip(status: SyncStatus) { private fun StatusPill(status: SyncStatus) {
val (icon, label, color) = when (status) { val (icon, label) = when (status) {
SyncStatus.SUCCESS -> Triple(Icons.Default.CheckCircle, "Synced", MaterialTheme.colorScheme.primaryContainer) SyncStatus.SUCCESS -> Pair(Icons.Default.CheckCircle, "Synced")
SyncStatus.SYNCING -> Triple(Icons.Default.Sync, "Syncing…", MaterialTheme.colorScheme.secondaryContainer) SyncStatus.SYNCING -> Pair(Icons.Default.Sync, "Syncing…")
SyncStatus.FAILED -> Triple(Icons.Default.Error, "Failed", MaterialTheme.colorScheme.errorContainer) SyncStatus.FAILED -> Pair(Icons.Default.Error, "Failed")
SyncStatus.CONFLICT -> Triple(Icons.Default.Warning, "Conflict", MaterialTheme.colorScheme.tertiaryContainer) SyncStatus.CONFLICT -> Pair(Icons.Default.Warning, "Conflict")
SyncStatus.PARTIAL -> Triple(Icons.Default.WarningAmber, "Partial", MaterialTheme.colorScheme.tertiaryContainer) SyncStatus.PARTIAL -> Pair(Icons.Default.WarningAmber, "Partial")
SyncStatus.IDLE -> Triple(Icons.Outlined.Circle, "Idle", MaterialTheme.colorScheme.surfaceVariant) SyncStatus.IDLE -> Pair(Icons.Outlined.Circle, "Idle")
}
val containerColor = status.accentColor
val contentColor = when (status) {
SyncStatus.IDLE -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.surface
}
Surface(
shape = RoundedCornerShape(50),
color = containerColor.copy(alpha = 0.15f),
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(icon, null, Modifier.size(12.dp), tint = containerColor)
Text(label, style = MaterialTheme.typography.labelSmall, color = containerColor)
}
} }
AssistChip(
onClick = {},
label = { Text(label) },
leadingIcon = { Icon(icon, null, Modifier.size(16.dp)) },
colors = AssistChipDefaults.assistChipColors(containerColor = color),
)
} }
@Composable @Composable
@@ -132,15 +205,49 @@ private fun EmptyState(modifier: Modifier = Modifier, onAdd: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Icon(Icons.Outlined.CloudSync, null, modifier = Modifier.size(80.dp), tint = MaterialTheme.colorScheme.primary) Icon(
Icons.Outlined.CloudSync,
null,
modifier = Modifier.size(100.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f),
)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text("No sync pairs yet", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Medium) Text("No sync pairs yet", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("Tap + to create your first sync", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(
"Tap + to create your first sync",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Button(onClick = onAdd) { Text("Add Sync Pair") } FilledTonalButton(onClick = onAdd) { Text("Add Sync Pair") }
} }
} }
private val fmt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) private val SyncStatus.accentColor: Color
private fun Instant.formatRelative(): String = fmt.format(atZone(ZoneId.systemDefault())) @Composable get() = when (this) {
SyncStatus.SUCCESS -> MaterialTheme.colorScheme.primary
SyncStatus.SYNCING -> MaterialTheme.colorScheme.secondary
SyncStatus.FAILED -> MaterialTheme.colorScheme.error
SyncStatus.CONFLICT,
SyncStatus.PARTIAL -> MaterialTheme.colorScheme.tertiary
SyncStatus.IDLE -> MaterialTheme.colorScheme.outline
}
private fun String.toDisplayPath(): String {
// For SAF content:// URIs, decode the last path segment (e.g. primary%3ASyncFlowTest → SyncFlowTest)
val decoded = java.net.URLDecoder.decode(this, "UTF-8")
return decoded.trimEnd('/').substringAfterLast('/').substringAfterLast(':').ifEmpty { decoded }
}
private fun Instant.toRelativeString(): String {
val diff = Duration.between(this, Instant.now()).abs()
return when {
diff.toMinutes() < 1 -> "Just now"
diff.toMinutes() < 60 -> "${diff.toMinutes()} min ago"
diff.toHours() < 24 -> "${diff.toHours()} hr ago"
diff.toDays() == 1L -> "Yesterday"
else -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.format(atZone(ZoneId.systemDefault()))
}
}
@@ -6,8 +6,15 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -19,8 +26,11 @@ import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syncflow.R
import com.syncflow.ui.home.HomeScreen import com.syncflow.ui.home.HomeScreen
import com.syncflow.ui.settings.SettingsScreen import com.syncflow.ui.settings.SettingsScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,11 +48,26 @@ fun MainShell(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( CenterAlignedTopAppBar(
title = { Text("SyncFlow", fontWeight = FontWeight.Bold) }, title = {
colors = TopAppBarDefaults.topAppBarColors( Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.ic_sync),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.width(8.dp))
Text(
"SyncFlow",
style = MaterialTheme.typography.titleLarge,
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
), ),
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars),
) )
}, },
bottomBar = { bottomBar = {
@@ -81,6 +106,8 @@ fun MainShell(
text = { Text("Add Sync") }, text = { Text("Add Sync") },
icon = { Icon(Icons.Default.Add, null) }, icon = { Icon(Icons.Default.Add, null) },
onClick = onAddPair, onClick = onAddPair,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
) )
} }
}, },
@@ -3,16 +3,24 @@ package com.syncflow.ui.pairdetail
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.data.db.entities.SyncEventEntity import com.syncflow.data.db.entities.SyncEventEntity
import com.syncflow.data.db.entities.SyncPairEntity
import com.syncflow.domain.model.SyncEventType import com.syncflow.domain.model.SyncEventType
import com.syncflow.domain.model.SyncStatus
import java.time.Duration
import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
@@ -36,7 +44,10 @@ fun PairDetailScreen(
title = { Text("Delete sync pair?") }, title = { Text("Delete sync pair?") },
text = { Text("This removes the pair and all sync history. Files are NOT deleted.") }, text = { Text("This removes the pair and all sync history. Files are NOT deleted.") },
confirmButton = { confirmButton = {
TextButton(onClick = { vm.delete(); onBack() }, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)) { TextButton(
onClick = { vm.delete(); onBack() },
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
) {
Text("Delete") Text("Delete")
} }
}, },
@@ -60,8 +71,12 @@ fun PairDetailScreen(
LazyColumn( LazyColumn(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
item {
pair?.let { p -> StatusBanner(p) }
}
item { item {
pair?.let { p -> pair?.let { p ->
InfoCard(p.localPath, p.remotePath, p.syncDirection.name, p.scheduleType.name) InfoCard(p.localPath, p.remotePath, p.syncDirection.name, p.scheduleType.name)
@@ -85,8 +100,22 @@ fun PairDetailScreen(
} }
item { item {
Text("Activity", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary) Row(
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 4.dp),
) {
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("Activity", style = MaterialTheme.typography.titleSmall)
}
HorizontalDivider(modifier = Modifier.padding(top = 4.dp))
} }
if (events.isEmpty()) { if (events.isEmpty()) {
@@ -105,24 +134,84 @@ fun PairDetailScreen(
} }
@Composable @Composable
private fun InfoCard(localPath: String, remotePath: String, direction: String, schedule: String) { private fun StatusBanner(pair: SyncPairEntity) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { val (icon, label, containerColor) = when (pair.lastSyncResult) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { SyncStatus.SUCCESS -> Triple(Icons.Default.CheckCircle, "Synced", MaterialTheme.colorScheme.primaryContainer)
InfoRow(Icons.Default.PhoneAndroid, "Local", localPath) SyncStatus.SYNCING -> Triple(Icons.Default.Sync, "Syncing…", MaterialTheme.colorScheme.secondaryContainer)
InfoRow(Icons.Default.Cloud, "Remote", remotePath) SyncStatus.FAILED -> Triple(Icons.Default.Error, "Failed", MaterialTheme.colorScheme.errorContainer)
InfoRow(Icons.Default.SwapHoriz, "Direction", direction) SyncStatus.CONFLICT -> Triple(Icons.Default.Warning, "Conflict", MaterialTheme.colorScheme.tertiaryContainer)
InfoRow(Icons.Default.Schedule, "Schedule", schedule) SyncStatus.PARTIAL -> Triple(Icons.Default.WarningAmber,"Partial", MaterialTheme.colorScheme.tertiaryContainer)
SyncStatus.IDLE -> Triple(Icons.Outlined.Circle, "Idle", MaterialTheme.colorScheme.surfaceVariant)
}
Surface(
color = containerColor,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(icon, null, modifier = Modifier.size(40.dp))
Spacer(Modifier.width(16.dp))
Column {
Text(label, style = MaterialTheme.typography.titleMedium)
pair.lastSyncAt?.let {
Text(it.toRelativeString(), style = MaterialTheme.typography.bodySmall)
}
}
} }
} }
} }
@Composable @Composable
private fun InfoRow(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, value: String) { private fun InfoCard(localPath: String, remotePath: String, direction: String, schedule: String) {
Row(verticalAlignment = Alignment.CenterVertically) { Card(
Icon(icon, null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary) modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
InfoRow(Icons.Default.PhoneAndroid, "Local", localPath.toDisplayPath())
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f))
InfoRow(Icons.Default.Cloud, "Remote", remotePath)
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f))
Row {
InfoRow(Icons.Default.SwapHoriz, "Direction", direction, modifier = Modifier.weight(1f))
InfoRow(Icons.Default.Schedule, "Schedule", schedule, modifier = Modifier.weight(1f))
}
}
}
}
@Composable
private fun InfoRow(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
value: String,
modifier: Modifier = Modifier,
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(28.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
icon,
null,
Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
}
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("$label: ", style = MaterialTheme.typography.labelMedium) Column {
Text(value, style = MaterialTheme.typography.bodySmall, modifier = Modifier.weight(1f)) Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, style = MaterialTheme.typography.bodySmall)
}
} }
} }
@@ -130,38 +219,65 @@ private fun InfoRow(icon: androidx.compose.ui.graphics.vector.ImageVector, label
private fun EventRow(event: SyncEventEntity) { private fun EventRow(event: SyncEventEntity) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
val zone = ZoneId.systemDefault() val zone = ZoneId.systemDefault()
val (icon, tint) = eventIcon(event.eventType) val dotColor = eventColor(event.eventType)
Row( Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(icon, null, Modifier.size(16.dp), tint = tint) // Colored dot indicator
Spacer(Modifier.width(8.dp)) Surface(
shape = RoundedCornerShape(50),
color = dotColor,
modifier = Modifier.size(8.dp),
) {}
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(event.filePath ?: event.message ?: event.eventType.name, style = MaterialTheme.typography.bodySmall) Text(
event.filePath ?: event.message ?: event.eventType.name,
style = MaterialTheme.typography.bodySmall,
)
event.message?.takeIf { event.filePath != null }?.let { event.message?.takeIf { event.filePath != null }?.let {
Text(it, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(
it,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
} }
Text(fmt.format(event.timestamp.atZone(zone)), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(
fmt.format(event.timestamp.atZone(zone)),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
} }
@Composable @Composable
private fun eventIcon(type: SyncEventType): Pair<androidx.compose.ui.graphics.vector.ImageVector, androidx.compose.ui.graphics.Color> { private fun eventColor(type: SyncEventType): Color = when (type) {
val green = MaterialTheme.colorScheme.primary SyncEventType.SYNC_STARTED -> MaterialTheme.colorScheme.primary
val red = MaterialTheme.colorScheme.error SyncEventType.SYNC_COMPLETED -> MaterialTheme.colorScheme.primary
val orange = MaterialTheme.colorScheme.tertiary SyncEventType.SYNC_FAILED -> MaterialTheme.colorScheme.error
val grey = MaterialTheme.colorScheme.onSurfaceVariant SyncEventType.FILE_UPLOADED -> MaterialTheme.colorScheme.secondary
return when (type) { SyncEventType.FILE_DOWNLOADED -> MaterialTheme.colorScheme.secondary
SyncEventType.SYNC_STARTED -> Pair(Icons.Default.PlayArrow, green) SyncEventType.FILE_DELETED -> MaterialTheme.colorScheme.tertiary
SyncEventType.SYNC_COMPLETED -> Pair(Icons.Default.CheckCircle, green) SyncEventType.FILE_SKIPPED -> MaterialTheme.colorScheme.onSurfaceVariant
SyncEventType.SYNC_FAILED -> Pair(Icons.Default.Error, red) SyncEventType.CONFLICT_DETECTED -> MaterialTheme.colorScheme.tertiary
SyncEventType.FILE_UPLOADED -> Pair(Icons.Default.Upload, green) SyncEventType.CONFLICT_RESOLVED -> MaterialTheme.colorScheme.primary
SyncEventType.FILE_DOWNLOADED -> Pair(Icons.Default.Download, green) }
SyncEventType.FILE_DELETED -> Pair(Icons.Default.Delete, orange)
SyncEventType.FILE_SKIPPED -> Pair(Icons.Default.SkipNext, grey) private fun String.toDisplayPath(): String {
SyncEventType.CONFLICT_DETECTED -> Pair(Icons.Default.Warning, orange) val decoded = java.net.URLDecoder.decode(this, "UTF-8")
SyncEventType.CONFLICT_RESOLVED -> Pair(Icons.Default.CheckCircle, green) return decoded.trimEnd('/').substringAfterLast('/').substringAfterLast(':').ifEmpty { decoded }
}
private fun Instant.toRelativeString(): String {
val diff = Duration.between(this, Instant.now()).abs()
return when {
diff.toMinutes() < 1 -> "Just now"
diff.toMinutes() < 60 -> "${diff.toMinutes()} min ago"
diff.toHours() < 24 -> "${diff.toHours()} hr ago"
diff.toDays() == 1L -> "Yesterday"
else -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.format(atZone(ZoneId.systemDefault()))
} }
} }
@@ -1,14 +1,17 @@
package com.syncflow.ui.settings package com.syncflow.ui.settings
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.data.db.entities.CloudAccountEntity import com.syncflow.data.db.entities.CloudAccountEntity
@@ -45,15 +48,18 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
item { item {
Row(verticalAlignment = Alignment.CenterVertically) { SectionHeader(title = "Cloud Accounts")
Text("Cloud Accounts", style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f)) Spacer(Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
FilledTonalButton(onClick = onAddAccount) { FilledTonalButton(onClick = onAddAccount) {
Icon(Icons.Default.Add, null, Modifier.size(18.dp)) Icon(Icons.Default.Add, null, Modifier.size(18.dp))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text("Add Account") Text("Add Account")
} }
} }
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
} }
if (accounts.isEmpty()) { if (accounts.isEmpty()) {
@@ -63,9 +69,22 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Icon(Icons.Default.CloudOff, null, Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) Icon(
Text("No accounts yet", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Icons.Default.CloudOff,
Text("Add a cloud account to start syncing", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) 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) { OutlinedButton(onClick = onAddAccount) {
Icon(Icons.Default.Add, null, Modifier.size(16.dp)) Icon(Icons.Default.Add, null, Modifier.size(16.dp))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
@@ -80,40 +99,102 @@ fun SettingsScreen(
} }
item { item {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(8.dp))
Text("Security", style = MaterialTheme.typography.titleMedium) SectionHeader(title = "Security")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) Spacer(Modifier.height(4.dp))
Row( Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, shape = RoundedCornerShape(16.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) { ) {
Column(modifier = Modifier.weight(1f)) { Row(
Text("Biometric Lock", style = MaterialTheme.typography.bodyMedium) modifier = Modifier.fillMaxWidth().padding(16.dp),
Text( verticalAlignment = Alignment.CenterVertically,
"Require biometrics when returning to app", ) {
style = MaterialTheme.typography.bodySmall, Column(modifier = Modifier.weight(1f)) {
color = MaterialTheme.colorScheme.onSurfaceVariant, 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) })
} }
Switch(checked = biometricEnabled, onCheckedChange = { vm.setBiometricLock(it) })
} }
} }
item { item {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(8.dp))
Text("About", style = MaterialTheme.typography.titleMedium) SectionHeader(title = "About")
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) Spacer(Modifier.height(4.dp))
Text("SyncFlow v${com.syncflow.BuildConfig.VERSION_NAME} — Free, no subscription.", style = MaterialTheme.typography.bodySmall) Card(
Text("Open source. No ads. No tracking.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) 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 @Composable
private fun AccountCard(acct: CloudAccountEntity, onDelete: () -> Unit) { private fun AccountCard(acct: CloudAccountEntity, onDelete: () -> Unit) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { 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) { Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(providerIcon(acct.providerType), null, Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary) // 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)) Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(acct.displayName, style = MaterialTheme.typography.bodyMedium) Text(acct.displayName, style = MaterialTheme.typography.bodyMedium)
@@ -138,22 +219,22 @@ private fun AccountCard(acct: CloudAccountEntity, onDelete: () -> Unit) {
private fun providerIcon(type: ProviderType) = when (type) { private fun providerIcon(type: ProviderType) = when (type) {
ProviderType.GOOGLE_DRIVE -> Icons.Default.Cloud ProviderType.GOOGLE_DRIVE -> Icons.Default.Cloud
ProviderType.DROPBOX -> Icons.Default.CloudQueue ProviderType.DROPBOX -> Icons.Default.CloudQueue
ProviderType.ONEDRIVE -> Icons.Default.CloudDone ProviderType.ONEDRIVE -> Icons.Default.CloudDone
ProviderType.WEBDAV -> Icons.Default.Storage ProviderType.WEBDAV -> Icons.Default.Storage
ProviderType.SFTP -> Icons.Default.Terminal ProviderType.SFTP -> Icons.Default.Terminal
ProviderType.NEXTCLOUD -> Icons.Default.CloudCircle ProviderType.NEXTCLOUD -> Icons.Default.CloudCircle
ProviderType.OWNCLOUD -> Icons.Default.CloudCircle ProviderType.OWNCLOUD -> Icons.Default.CloudCircle
ProviderType.SFTPGO -> Icons.Default.Storage ProviderType.SFTPGO -> Icons.Default.Storage
} }
private fun friendlyProviderName(type: ProviderType) = when (type) { private fun friendlyProviderName(type: ProviderType) = when (type) {
ProviderType.GOOGLE_DRIVE -> "Google Drive" ProviderType.GOOGLE_DRIVE -> "Google Drive"
ProviderType.DROPBOX -> "Dropbox" ProviderType.DROPBOX -> "Dropbox"
ProviderType.ONEDRIVE -> "OneDrive" ProviderType.ONEDRIVE -> "OneDrive"
ProviderType.WEBDAV -> "WebDAV" ProviderType.WEBDAV -> "WebDAV"
ProviderType.SFTP -> "SFTP" ProviderType.SFTP -> "SFTP"
ProviderType.NEXTCLOUD -> "Nextcloud" ProviderType.NEXTCLOUD -> "Nextcloud"
ProviderType.OWNCLOUD -> "ownCloud" ProviderType.OWNCLOUD -> "ownCloud"
ProviderType.SFTPGO -> "SFTPGo" ProviderType.SFTPGO -> "SFTPGo"
} }
@@ -2,8 +2,27 @@ package com.syncflow.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val SyncBlue = Color(0xFF2196F3) // Primary — indigo
val SyncGreen = Color(0xFF4CAF50) val Indigo600 = Color(0xFF4F46E5)
val SyncOrange = Color(0xFFFF9800) val Indigo900 = Color(0xFF312E81)
val SyncRed = Color(0xFFF44336) val Indigo100 = Color(0xFFE0E7FF)
val SyncPurple = Color(0xFF9C27B0) val Indigo50 = Color(0xFFEEF2FF)
// Secondary — teal
val Teal600 = Color(0xFF0D9488)
val Teal100 = Color(0xFFCCFBF1)
// Tertiary — amber
val Amber500 = Color(0xFFF59E0B)
val Amber100 = Color(0xFFFEF3C7)
// Neutrals
val Slate50 = Color(0xFFF8FAFC)
val Slate100 = Color(0xFFF1F5F9)
val Slate200 = Color(0xFFE2E8F0)
val Slate600 = Color(0xFF475569)
val Slate900 = Color(0xFF0F172A)
// Semantic
val GreenSuccess = Color(0xFF16A34A)
val RedError = Color(0xFFDC2626)
@@ -1,50 +1,77 @@
package com.syncflow.ui.theme package com.syncflow.ui.theme
import android.app.Activity import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
private val LightColors = lightColorScheme( private val LightColors = lightColorScheme(
primary = SyncBlue, primary = Indigo600,
onPrimary = androidx.compose.ui.graphics.Color.White, onPrimary = Color.White,
secondary = SyncGreen, primaryContainer = Indigo100,
tertiary = SyncPurple, onPrimaryContainer = Indigo900,
secondary = Teal600,
onSecondary = Color.White,
secondaryContainer = Teal100,
tertiary = Amber500,
tertiaryContainer = Amber100,
background = Slate50,
surface = Color.White,
surfaceVariant = Slate100,
onSurfaceVariant = Slate600,
error = RedError,
errorContainer = Color(0xFFFEE2E2),
outline = Slate200,
) )
private val DarkColors = darkColorScheme( private val DarkColors = darkColorScheme(
primary = SyncBlue, primary = Color(0xFF818CF8),
secondary = SyncGreen, onPrimary = Indigo900,
tertiary = SyncPurple, primaryContainer = Color(0xFF3730A3),
onPrimaryContainer = Indigo100,
secondary = Color(0xFF2DD4BF),
onSecondary = Color(0xFF003731),
secondaryContainer = Color(0xFF00504A),
tertiary = Amber500,
tertiaryContainer = Color(0xFF92400E),
background = Color(0xFF0F0F1A),
surface = Color(0xFF1A1A2E),
surfaceVariant = Color(0xFF252538),
onSurfaceVariant = Color(0xFF94A3B8),
error = Color(0xFFF87171),
errorContainer = Color(0xFF7F1D1D),
outline = Color(0xFF334155),
)
private val AppTypography = Typography(
titleLarge = TextStyle(fontWeight = FontWeight.Bold, fontSize = 22.sp, letterSpacing = (-0.5).sp),
titleMedium = TextStyle(fontWeight = FontWeight.SemiBold, fontSize = 16.sp, letterSpacing = (-0.25).sp),
titleSmall = TextStyle(fontWeight = FontWeight.SemiBold, fontSize = 14.sp, letterSpacing = 0.sp),
labelMedium = TextStyle(fontWeight = FontWeight.Medium, fontSize = 12.sp, letterSpacing = 0.1.sp),
labelSmall = TextStyle(fontWeight = FontWeight.Medium, fontSize = 11.sp, letterSpacing = 0.1.sp),
) )
@Composable @Composable
fun SyncFlowTheme( fun SyncFlowTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val colorScheme = when { val colorScheme = if (darkTheme) DarkColors else LightColors
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val ctx = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
}
darkTheme -> DarkColors
else -> LightColors
}
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb() WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
} }
} }
MaterialTheme(colorScheme = colorScheme, typography = Typography(), content = content) MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content)
} }
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#312E81"
android:endColor="#6366F1"/>
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01-0.25,1.97-0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0-4.42-3.58-8-8-8z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M12,18c-3.31,0-6,-2.69-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4-4,-4v3z"/>
</vector>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_sync"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.12 VERSION_NAME=1.0.13
VERSION_CODE=13 VERSION_CODE=14