diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
index f249599..622e2ed 100644
--- a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
+++ b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
@@ -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,
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
)
diff --git a/app/src/main/java/sh/sar/isodroid/ui/components/FileContextMenu.kt b/app/src/main/java/sh/sar/isodroid/ui/components/FileContextMenu.kt
new file mode 100644
index 0000000..72c3e22
--- /dev/null
+++ b/app/src/main/java/sh/sar/isodroid/ui/components/FileContextMenu.kt
@@ -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")
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt b/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt
index ba92760..cf7148f 100644
--- a/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt
+++ b/app/src/main/java/sh/sar/isodroid/ui/screens/MainScreen.kt
@@ -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(null) }
var showMountDialog by remember { mutableStateOf(false) }
var showCreateImgDialog by remember { mutableStateOf(false) }
+ var contextMenuFile by remember { mutableStateOf(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)
+ }
+ )
+ }
}
diff --git a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
index 123302f..ad6d78a 100644
--- a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
@@ -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) }
}