delete and rename files

This commit is contained in:
2026-03-10 04:27:42 +05:00
parent 800f0fa15a
commit 0136b7b9f2
5 changed files with 321 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -26,6 +27,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import sh.sar.isodroid.data.IsoFile
@@ -35,10 +38,13 @@ fun FileBrowser(
files: List<IsoFile>,
currentPath: String,
onFileClick: (IsoFile) -> Unit,
onFileLongClick: (IsoFile) -> Unit,
onNavigateUp: () -> Unit,
canNavigateUp: Boolean,
modifier: Modifier = Modifier
) {
val hapticFeedback = LocalHapticFeedback.current
Column(modifier = modifier.fillMaxSize()) {
if (files.isEmpty()) {
Column(
@@ -78,7 +84,8 @@ fun FileBrowser(
name = "..",
size = "",
isDirectory = true,
onClick = onNavigateUp
onClick = onNavigateUp,
onLongClick = null
)
}
}
@@ -88,7 +95,11 @@ fun FileBrowser(
name = file.name,
size = file.formattedSize,
isIso = file.name.lowercase().endsWith(".iso"),
onClick = { onFileClick(file) }
onClick = { onFileClick(file) },
onLongClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onFileLongClick(file)
}
)
}
}
@@ -96,18 +107,23 @@ fun FileBrowser(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FileItem(
name: String,
size: String,
isDirectory: Boolean = false,
isIso: Boolean = true,
onClick: () -> Unit
onClick: () -> Unit,
onLongClick: (() -> Unit)?
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)

View File

@@ -0,0 +1,197 @@
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import sh.sar.isodroid.data.IsoFile
@Composable
fun FileContextMenu(
file: IsoFile,
onDismiss: () -> Unit,
onRename: (String) -> Unit,
onDelete: () -> Unit
) {
var showRenameDialog by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
if (showRenameDialog) {
RenameDialog(
currentName = file.name,
onDismiss = { showRenameDialog = false },
onConfirm = { newName ->
showRenameDialog = false
onRename(newName)
onDismiss()
}
)
} else if (showDeleteConfirm) {
DeleteConfirmDialog(
fileName = file.name,
onDismiss = { showDeleteConfirm = false },
onConfirm = {
showDeleteConfirm = false
onDelete()
onDismiss()
}
)
} else {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = file.name,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
text = {
Column {
MenuItem(
icon = { Icon(Icons.Default.Edit, contentDescription = null) },
text = "Rename",
onClick = { showRenameDialog = true }
)
Spacer(modifier = Modifier.height(8.dp))
MenuItem(
icon = {
Icon(
Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
},
text = "Delete",
textColor = MaterialTheme.colorScheme.error,
onClick = { showDeleteConfirm = true }
)
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
}
@Composable
private fun MenuItem(
icon: @Composable () -> Unit,
text: String,
textColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon()
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = textColor
)
}
}
@Composable
private fun RenameDialog(
currentName: String,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
// Extract name without extension
val extension = if (currentName.contains(".")) {
"." + currentName.substringAfterLast(".")
} else ""
val nameWithoutExtension = currentName.removeSuffix(extension)
var newName by remember { mutableStateOf(nameWithoutExtension) }
val isValid = newName.isNotBlank() && !newName.contains("/")
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Rename") },
text = {
Column {
OutlinedTextField(
value = newName,
onValueChange = { newName = it.replace("/", "") },
label = { Text("File Name") },
suffix = { Text(extension) },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
TextButton(
onClick = { onConfirm(newName + extension) },
enabled = isValid && newName != nameWithoutExtension
) {
Text("Rename")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@Composable
private fun DeleteConfirmDialog(
fileName: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Delete File") },
text = {
Text("Are you sure you want to delete \"$fileName\"? This action cannot be undone.")
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Delete", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}

View File

@@ -39,6 +39,7 @@ import kotlinx.coroutines.launch
import sh.sar.isodroid.data.IsoFile
import sh.sar.isodroid.data.MountOptions
import sh.sar.isodroid.ui.components.CreateImgDialog
import sh.sar.isodroid.ui.components.FileContextMenu
import sh.sar.isodroid.ui.components.FileBrowser
import sh.sar.isodroid.ui.components.MountDialog
import sh.sar.isodroid.ui.components.StatusCard
@@ -58,6 +59,7 @@ fun MainScreen(
var selectedFile by remember { mutableStateOf<IsoFile?>(null) }
var showMountDialog by remember { mutableStateOf(false) }
var showCreateImgDialog by remember { mutableStateOf(false) }
var contextMenuFile by remember { mutableStateOf<IsoFile?>(null) }
// Show error messages
LaunchedEffect(uiState.errorMessage) {
@@ -169,6 +171,9 @@ fun MainScreen(
selectedFile = file
showMountDialog = true
},
onFileLongClick = { file ->
contextMenuFile = file
},
onNavigateUp = { viewModel.navigateUp() },
canNavigateUp = viewModel.canNavigateUp(),
modifier = Modifier.weight(1f)
@@ -214,4 +219,18 @@ fun MainScreen(
}
)
}
// File context menu (long press)
contextMenuFile?.let { file ->
FileContextMenu(
file = file,
onDismiss = { contextMenuFile = null },
onRename = { newName ->
viewModel.renameFile(file, newName)
},
onDelete = {
viewModel.deleteFile(file)
}
)
}
}

View File

@@ -501,6 +501,40 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun renameFile(file: IsoFile, newName: String) {
viewModelScope.launch {
val oldPath = file.path
val newPath = "${oldPath.substringBeforeLast("/")}/$newName"
// Check if new file already exists
val checkResult = RootManager.executeCommand("test -f \"$newPath\" && echo exists")
if (checkResult.output.trim() == "exists") {
_uiState.update { it.copy(errorMessage = "File already exists: $newName") }
return@launch
}
val result = RootManager.executeCommand("mv \"$oldPath\" \"$newPath\"")
if (result.success) {
_uiState.update { it.copy(successMessage = "Renamed to $newName") }
loadFiles()
} else {
_uiState.update { it.copy(errorMessage = "Failed to rename file") }
}
}
}
fun deleteFile(file: IsoFile) {
viewModelScope.launch {
val result = RootManager.executeCommand("rm -f \"${file.path}\"")
if (result.success) {
_uiState.update { it.copy(successMessage = "Deleted ${file.name}") }
loadFiles()
} else {
_uiState.update { it.copy(errorMessage = "Failed to delete file") }
}
}
}
fun clearError() {
_uiState.update { it.copy(errorMessage = null) }
}