Compare commits

..

1 Commits

Author SHA1 Message Date
amir 742f634084 v1.0.26: fix multi-selection reactivity, redesign icon, security review
Fix multi-selection: selectedKeys exposed as StateFlow, collected in
FilesScreen so checkboxes and highlights update correctly on every tap.
fileKey() made public so UI can check membership without ViewModel calls.

Icon: white cloud body with two cyan/teal circular sync arcs (AutoSync
style), deep blue-to-teal gradient background.

Security review clean: no hardcoded credentials, cleartext blocked by
network_security_config, allowBackup=false, path traversal guards in
place on both server responses and local resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 02:45:43 +00:00
5 changed files with 86 additions and 39 deletions
@@ -39,8 +39,9 @@ fun FilesScreen(
val selectedPair by vm.selectedPair.collectAsState() val selectedPair by vm.selectedPair.collectAsState()
val files by vm.files.collectAsState() val files by vm.files.collectAsState()
val isDownloading by vm.isDownloading.collectAsState() val isDownloading by vm.isDownloading.collectAsState()
val isSelectionMode by vm.isSelectionMode.collectAsState() val selectedKeys by vm.selectedKeys.collectAsState()
val selectedCount by vm.selectedCount.collectAsState() val isSelectionMode = selectedKeys.isNotEmpty()
val selectedCount = selectedKeys.size
val context = LocalContext.current val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -252,12 +253,11 @@ fun FilesScreen(
} }
} }
items(dirFiles, key = { "${it.syncPairId}_${it.relativePath}" }) { file -> items(dirFiles, key = { "${it.syncPairId}_${it.relativePath}" }) { file ->
val selected = vm.isSelected(file)
FileRow( FileRow(
file = file, file = file,
isInSubDir = dir.isNotEmpty() && !isSelectionMode, isInSubDir = dir.isNotEmpty() && !isSelectionMode,
isSelectionMode = isSelectionMode, isSelectionMode = isSelectionMode,
isSelected = selected, isSelected = vm.fileKey(file) in selectedKeys,
vm = vm, vm = vm,
) )
HorizontalDivider( HorizontalDivider(
@@ -60,6 +60,7 @@ class FilesViewModel @Inject constructor(
val isDownloading: StateFlow<Boolean> = _isDownloading val isDownloading: StateFlow<Boolean> = _isDownloading
private val _selectedKeys = MutableStateFlow<Set<String>>(emptySet()) private val _selectedKeys = MutableStateFlow<Set<String>>(emptySet())
val selectedKeys: StateFlow<Set<String>> = _selectedKeys
val isSelectionMode: StateFlow<Boolean> = _selectedKeys.map { it.isNotEmpty() } val isSelectionMode: StateFlow<Boolean> = _selectedKeys.map { it.isNotEmpty() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
val selectedCount: StateFlow<Int> = _selectedKeys.map { it.size } val selectedCount: StateFlow<Int> = _selectedKeys.map { it.size }
@@ -152,7 +153,7 @@ class FilesViewModel @Inject constructor(
} }
} }
private fun fileKey(file: SyncFileStateEntity) = "${file.syncPairId}:${file.relativePath}" fun fileKey(file: SyncFileStateEntity) = "${file.syncPairId}:${file.relativePath}"
// ── Download-then-open/share ────────────────────────────────────────────── // ── Download-then-open/share ──────────────────────────────────────────────
@@ -1,10 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <vector xmlns:android="http://schemas.android.com/apk/res/android"
<gradient xmlns:aapt="http://schemas.android.com/aapt"
android:type="radial" android:width="108dp"
android:gradientRadius="80%" android:height="108dp"
android:centerX="0.5" android:viewportWidth="108"
android:centerY="0.4" android:viewportHeight="108">
android:startColor="#1B2A3B"
android:endColor="#06090E"/> <!-- Deep blue-to-teal gradient background matching reference icon -->
</shape> <path
android:pathData="M0,0 H108 V108 H0 Z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="0" android:startY="0"
android:endX="108" android:endY="108"
android:startColor="#1565C0"
android:endColor="#00897B"/>
</aapt:attr>
</path>
<!-- Subtle radial highlight in upper-right -->
<path
android:pathData="M108,0 A90,90 0 0,1 108,90 Z"
android:fillAlpha="0.18">
<aapt:attr name="android:fillColor">
<gradient
android:type="radial"
android:gradientRadius="80"
android:centerX="85" android:centerY="23"
android:startColor="#80DEEA"
android:endColor="#00000000"/>
</aapt:attr>
</path>
</vector>
@@ -6,45 +6,65 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<!-- <!-- Cloud body: white, centred at (54,56). Composed of three arc bumps. -->
Circular sync icon (AutoSync-style): two 170° arcs forming a clockwise
rotation. Arc 1 sweeps over the bottom (right→bottom→left), Arc 2 over the
top (left→top→right). Arrowheads at the 9-o'clock and 3-o'clock gaps.
Circle radius 26, center (54,54).
-->
<!-- Arc 1: right → bottom → left (CW, small arc 170°) -->
<path <path
android:pathData="M 79.9,56.3 A 26,26 0 0,1 28.1,56.3" android:pathData="
M 38,67
A 9,9 0 0,1 38,49
A 9,9 0 0,1 47,40.5
A 12,12 0 0,1 68,42
A 8,8 0 0,1 76,53
A 8,8 0 0,1 70,67
Z"
android:fillColor="#FFFFFF"/>
<!-- Cloud drop shadow -->
<path
android:pathData="
M 38,69
A 9,9 0 0,1 38,51
A 9,9 0 0,1 47,42.5
A 12,12 0 0,1 68,44
A 8,8 0 0,1 76,55
A 8,8 0 0,1 70,69
Z"
android:fillColor="#000000"
android:fillAlpha="0.10"/>
<!-- Sync arc 1: lower half CW, cyan to teal -->
<path
android:pathData="M 49.14,81.57 A 28,28 0 1,1 49.14,26.43"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeWidth="9" android:strokeWidth="5.5"
android:strokeLineCap="round"> android:strokeLineCap="round">
<aapt:attr name="android:strokeColor"> <aapt:attr name="android:strokeColor">
<gradient android:type="linear" <gradient android:type="linear"
android:startX="79.9" android:startY="56.3" android:startX="49.14" android:startY="81.57"
android:endX="28.1" android:endY="56.3" android:endX="49.14" android:endY="26.43"
android:startColor="#40C4FF" android:startColor="#40C4FF"
android:endColor="#00E5FF"/> android:endColor="#00BFA5"/>
</aapt:attr> </aapt:attr>
</path> </path>
<!-- Arrowhead at left side (175°), pointing upward --> <!-- Arrowhead at end of arc 1 (near 260 deg) -->
<path android:pathData="M 23.9,65.7 L 28.1,56.3 L 33.9,64.8 Z" android:fillColor="#00E5FF"/> <path android:pathData="M 42.5,30.5 L 49.14,26.43 L 46.0,34.5 Z"
android:fillColor="#00BFA5"/>
<!-- Arc 2: left → top → right (CW, small arc 170°) --> <!-- Sync arc 2: upper half CW, teal to cyan -->
<path <path
android:pathData="M 28.1,51.7 A 26,26 0 0,1 79.9,51.7" android:pathData="M 58.86,26.43 A 28,28 0 1,1 58.86,81.57"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeWidth="9" android:strokeWidth="5.5"
android:strokeLineCap="round"> android:strokeLineCap="round">
<aapt:attr name="android:strokeColor"> <aapt:attr name="android:strokeColor">
<gradient android:type="linear" <gradient android:type="linear"
android:startX="28.1" android:startY="51.7" android:startX="58.86" android:startY="26.43"
android:endX="79.9" android:endY="51.7" android:endX="58.86" android:endY="81.57"
android:startColor="#00E5FF" android:startColor="#00BFA5"
android:endColor="#40C4FF"/> android:endColor="#40C4FF"/>
</aapt:attr> </aapt:attr>
</path> </path>
<!-- Arrowhead at right side (355°), pointing downward --> <!-- Arrowhead at end of arc 2 (near 80 deg) -->
<path android:pathData="M 84.1,42.3 L 79.9,51.7 L 74.1,43.2 Z" android:fillColor="#40C4FF"/> <path android:pathData="M 65.5,77.5 L 58.86,81.57 L 62.0,73.5 Z"
android:fillColor="#40C4FF"/>
</vector> </vector>
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.25 VERSION_NAME=1.0.26
VERSION_CODE=26 VERSION_CODE=27