@@ -10,6 +10,8 @@ import androidx.compose.animation.core.rememberInfiniteTransition
1010import androidx.compose.animation.core.tween
1111import androidx.compose.foundation.background
1212import androidx.compose.foundation.border
13+ import androidx.compose.foundation.ExperimentalFoundationApi
14+ import androidx.compose.foundation.combinedClickable
1315import androidx.compose.foundation.layout.Arrangement
1416import androidx.compose.foundation.layout.Box
1517import androidx.compose.foundation.layout.Column
@@ -29,13 +31,19 @@ import androidx.compose.material.icons.Icons
2931import androidx.compose.material.icons.automirrored.filled.ArrowBack
3032import androidx.compose.material.icons.filled.Add
3133import androidx.compose.material.icons.filled.Album
34+ import androidx.compose.material.icons.filled.CheckCircle
35+ import androidx.compose.material.icons.filled.Close
36+ import androidx.compose.material.icons.filled.Delete
37+ import androidx.compose.material.icons.filled.MoreVert
3238import androidx.compose.material.icons.filled.Storage
3339import androidx.compose.material3.AlertDialog
3440import androidx.compose.material3.Button
3541import androidx.compose.material3.ButtonDefaults
3642import androidx.compose.material3.Card
3743import androidx.compose.material3.CardDefaults
3844import androidx.compose.material3.CircularProgressIndicator
45+ import androidx.compose.material3.DropdownMenu
46+ import androidx.compose.material3.DropdownMenuItem
3947import androidx.compose.material3.ExperimentalMaterial3Api
4048import androidx.compose.material3.FloatingActionButton
4149import androidx.compose.material3.Icon
@@ -123,7 +131,12 @@ fun ImageManagerScreen(
123131 var showCreateSheet by remember { mutableStateOf(false ) }
124132 var deleteTarget by remember { mutableStateOf<DiskImage ?>(null ) }
125133 var exportingPath by remember { mutableStateOf<String ?>(null ) }
134+ var selected by remember { mutableStateOf(emptySet<String >()) }
135+ var showMenu by remember { mutableStateOf(false ) }
136+ var showClearConfirm by remember { mutableStateOf(false ) }
137+ var showBatchDelete by remember { mutableStateOf(false ) }
126138
139+ val inSelectionMode = selected.isNotEmpty()
127140 val exportSuccessMsg = stringResource(R .string.images_export_success)
128141 val exportFailedMsg = stringResource(R .string.images_export_failed)
129142
@@ -167,29 +180,123 @@ fun ImageManagerScreen(
167180 )
168181 }
169182
183+ if (showClearConfirm) {
184+ val count = images.count { it.name !in mountedNames }
185+ AlertDialog (
186+ onDismissRequest = { showClearConfirm = false },
187+ title = { Text (stringResource(R .string.images_clear_unmounted_title)) },
188+ text = { Text (stringResource(R .string.images_clear_unmounted_message, count)) },
189+ confirmButton = {
190+ TextButton (onClick = {
191+ showClearConfirm = false
192+ scope.launch {
193+ withContext(Dispatchers .IO ) {
194+ images.filter { it.name !in mountedNames }
195+ .forEach { it.file.delete() }
196+ }
197+ selected = emptySet()
198+ refreshTrigger++
199+ }
200+ }) {
201+ Text (stringResource(R .string.action_delete), color = MaterialTheme .colorScheme.error)
202+ }
203+ },
204+ dismissButton = {
205+ TextButton (onClick = { showClearConfirm = false }) {
206+ Text (stringResource(R .string.action_cancel))
207+ }
208+ }
209+ )
210+ }
211+
212+ if (showBatchDelete) {
213+ AlertDialog (
214+ onDismissRequest = { showBatchDelete = false },
215+ title = { Text (stringResource(R .string.images_batch_delete_title, selected.size)) },
216+ text = { Text (stringResource(R .string.images_batch_delete_message, selected.size)) },
217+ confirmButton = {
218+ TextButton (onClick = {
219+ val toDelete = selected.toSet()
220+ showBatchDelete = false
221+ scope.launch {
222+ withContext(Dispatchers .IO ) {
223+ toDelete.forEach { path ->
224+ val f = java.io.File (path)
225+ if (f.name !in mountedNames) f.delete()
226+ }
227+ }
228+ selected = emptySet()
229+ refreshTrigger++
230+ }
231+ }) {
232+ Text (stringResource(R .string.action_delete), color = MaterialTheme .colorScheme.error)
233+ }
234+ },
235+ dismissButton = {
236+ TextButton (onClick = { showBatchDelete = false }) {
237+ Text (stringResource(R .string.action_cancel))
238+ }
239+ }
240+ )
241+ }
242+
170243 Scaffold (
171244 topBar = {
172245 TopAppBar (
173- title = { Text (stringResource(R .string.images_title)) },
246+ title = {
247+ if (inSelectionMode) Text (stringResource(R .string.images_selected_count, selected.size))
248+ else Text (stringResource(R .string.images_title))
249+ },
174250 colors = TopAppBarDefaults .topAppBarColors(
175251 containerColor = MaterialTheme .colorScheme.surface
176252 ),
177253 navigationIcon = {
178- IconButton (onClick = onBack) {
179- Icon (
180- Icons .AutoMirrored .Filled .ArrowBack ,
181- contentDescription = stringResource(R .string.action_back)
182- )
254+ if (inSelectionMode) {
255+ IconButton (onClick = { selected = emptySet() }) {
256+ Icon (Icons .Filled .Close , contentDescription = stringResource(R .string.action_close))
257+ }
258+ } else {
259+ IconButton (onClick = onBack) {
260+ Icon (Icons .AutoMirrored .Filled .ArrowBack , contentDescription = stringResource(R .string.action_back))
261+ }
262+ }
263+ },
264+ actions = {
265+ if (! inSelectionMode) {
266+ IconButton (onClick = { showCreateSheet = true }) {
267+ Icon (Icons .Filled .Add , contentDescription = stringResource(R .string.images_create_new))
268+ }
269+ Box {
270+ IconButton (onClick = { showMenu = true }) {
271+ Icon (Icons .Filled .MoreVert , contentDescription = stringResource(R .string.action_menu))
272+ }
273+ DropdownMenu (expanded = showMenu, onDismissRequest = { showMenu = false }) {
274+ DropdownMenuItem (
275+ text = { Text (stringResource(R .string.images_clear_unmounted)) },
276+ onClick = {
277+ showMenu = false
278+ if (images.isEmpty() || images.none { it.name !in mountedNames }) {
279+ Toast .makeText(context, context.getString(R .string.images_no_unmounted), Toast .LENGTH_SHORT ).show()
280+ } else {
281+ showClearConfirm = true
282+ }
283+ }
284+ )
285+ }
286+ }
183287 }
184288 }
185289 )
186290 },
187291 floatingActionButton = {
188- FloatingActionButton (onClick = { showCreateSheet = true }) {
189- Icon (
190- Icons .Filled .Add ,
191- contentDescription = stringResource(R .string.images_create_new)
192- )
292+ if (inSelectionMode) {
293+ FloatingActionButton (
294+ onClick = { showBatchDelete = true },
295+ containerColor = MaterialTheme .colorScheme.error,
296+ contentColor = MaterialTheme .colorScheme.onError
297+ ) {
298+ Icon (Icons .Filled .Delete , contentDescription = stringResource(R .string.action_delete))
299+ }
193300 }
194301 }
195302 ) { padding ->
@@ -245,10 +352,13 @@ fun ImageManagerScreen(
245352 modifier = Modifier .fillMaxSize().padding(padding)
246353 ) {
247354 items(images, key = { it.file.absolutePath }) { image ->
355+ val isMounted = image.name in mountedNames
248356 ImageCard (
249357 image = image,
250- isMounted = image.name in mountedNames ,
358+ isMounted = isMounted ,
251359 isExporting = exportingPath == image.file.absolutePath,
360+ inSelectionMode = inSelectionMode,
361+ isSelected = image.file.absolutePath in selected,
252362 onMount = {
253363 onMount(
254364 DeviceInfo (
@@ -280,7 +390,22 @@ fun ImageManagerScreen(
280390 ).show()
281391 }
282392 },
283- onDelete = { deleteTarget = image }
393+ onDelete = { deleteTarget = image },
394+ onToggleSelect = {
395+ if (! isMounted) {
396+ val path = image.file.absolutePath
397+ selected = if (path in selected) selected - path else selected + path
398+ }
399+ }
400+ )
401+ }
402+ item {
403+ Spacer (Modifier .height(4 .dp))
404+ Text (
405+ stringResource(R .string.images_storage_hint),
406+ style = MaterialTheme .typography.bodySmall,
407+ color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.4f ),
408+ modifier = Modifier .fillMaxWidth().padding(horizontal = 4 .dp)
284409 )
285410 }
286411 }
@@ -300,14 +425,18 @@ fun ImageManagerScreen(
300425 }
301426}
302427
428+ @OptIn(ExperimentalFoundationApi ::class )
303429@Composable
304430private fun ImageCard (
305431 image : DiskImage ,
306432 isMounted : Boolean ,
307433 isExporting : Boolean ,
434+ inSelectionMode : Boolean ,
435+ isSelected : Boolean ,
308436 onMount : () -> Unit ,
309437 onExport : () -> Unit ,
310438 onDelete : () -> Unit ,
439+ onToggleSelect : () -> Unit ,
311440 modifier : Modifier = Modifier
312441) {
313442 val glow = rememberInfiniteTransition(label = " card" )
@@ -324,17 +453,31 @@ private fun ImageCard(
324453 Card (
325454 modifier = modifier
326455 .fillMaxWidth()
327- .border(1 .dp, tint.copy(alpha = borderAlpha), RoundedCornerShape (12 .dp)),
456+ .then(
457+ if (isSelected) Modifier .border(2 .dp, MaterialTheme .colorScheme.primary, RoundedCornerShape (12 .dp))
458+ else Modifier .border(1 .dp, tint.copy(alpha = borderAlpha), RoundedCornerShape (12 .dp))
459+ )
460+ .combinedClickable(
461+ onClick = { if (inSelectionMode && ! isMounted) onToggleSelect() },
462+ onLongClick = { if (! isMounted) onToggleSelect() }
463+ ),
328464 colors = CardDefaults .cardColors(
329465 containerColor = MaterialTheme .colorScheme.surfaceContainer
330466 ),
331467 elevation = CardDefaults .cardElevation(defaultElevation = 4 .dp)
332468 ) {
333469 Column (modifier = Modifier .padding(16 .dp)) {
334470 Row (verticalAlignment = Alignment .CenterVertically ) {
335- val icon = if (image.extension == " iso" ) Icons .Filled .Album
336- else Icons .Filled .Storage
337- Icon (icon, null , modifier = Modifier .size(28 .dp), tint = tint)
471+ val icon = when {
472+ isSelected -> Icons .Filled .CheckCircle
473+ image.extension == " iso" -> Icons .Filled .Album
474+ else -> Icons .Filled .Storage
475+ }
476+ Icon (
477+ icon, null ,
478+ modifier = Modifier .size(28 .dp),
479+ tint = if (isSelected) MaterialTheme .colorScheme.primary else tint
480+ )
338481 Spacer (Modifier .width(12 .dp))
339482
340483 Column (modifier = Modifier .weight(1f )) {
@@ -379,44 +522,46 @@ private fun ImageCard(
379522 }
380523 }
381524
382- Spacer (Modifier .height(12 .dp))
383-
384- Row (
385- modifier = Modifier .fillMaxWidth(),
386- horizontalArrangement = Arrangement .spacedBy(8 .dp)
387- ) {
388- OutlinedButton (
389- onClick = onMount,
390- enabled = ! isMounted,
391- modifier = Modifier .weight(1f ),
392- contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
393- ) { Text (stringResource(R .string.action_mount)) }
525+ if (! inSelectionMode) {
526+ Spacer (Modifier .height(12 .dp))
394527
395- OutlinedButton (
396- onClick = onExport,
397- enabled = ! isExporting,
398- modifier = Modifier .weight(1f ),
399- contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
528+ Row (
529+ modifier = Modifier .fillMaxWidth(),
530+ horizontalArrangement = Arrangement .spacedBy(8 .dp)
400531 ) {
401- if (isExporting) {
402- CircularProgressIndicator (
403- Modifier .size(16 .dp),
404- strokeWidth = 2 .dp
405- )
406- Spacer (Modifier .width(4 .dp))
532+ OutlinedButton (
533+ onClick = onMount,
534+ enabled = ! isMounted,
535+ modifier = Modifier .weight(1f ),
536+ contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
537+ ) { Text (stringResource(R .string.action_mount)) }
538+
539+ OutlinedButton (
540+ onClick = onExport,
541+ enabled = ! isExporting,
542+ modifier = Modifier .weight(1f ),
543+ contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
544+ ) {
545+ if (isExporting) {
546+ CircularProgressIndicator (
547+ Modifier .size(16 .dp),
548+ strokeWidth = 2 .dp
549+ )
550+ Spacer (Modifier .width(4 .dp))
551+ }
552+ Text (stringResource(R .string.action_export))
407553 }
408- Text (stringResource(R .string.action_export))
409- }
410554
411- OutlinedButton (
412- onClick = onDelete,
413- enabled = ! isMounted,
414- modifier = Modifier .weight(1f ),
415- colors = ButtonDefaults .outlinedButtonColors(
416- contentColor = MaterialTheme .colorScheme.error
417- ),
418- contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
419- ) { Text (stringResource(R .string.action_delete)) }
555+ OutlinedButton (
556+ onClick = onDelete,
557+ enabled = ! isMounted,
558+ modifier = Modifier .weight(1f ),
559+ colors = ButtonDefaults .outlinedButtonColors(
560+ contentColor = MaterialTheme .colorScheme.error
561+ ),
562+ contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 6 .dp)
563+ ) { Text (stringResource(R .string.action_delete)) }
564+ }
420565 }
421566 }
422567 }
0 commit comments