Desktop: replace tiny Swing folder chooser with in-app themed picker
JFileChooser rendered unusably small on this HiDPI/Wayland compositor and had no resize grip. Replace it with a Compose dialog matching the app's navy+teal skin: breadcrumb row (Home + path steps), scrollable list of subfolders as cards (dot-dirs hidden), '..' row, and a confirm button naming the selected folder. Scales with the app's Ctrl+-/= zoom since it lives inside the Compose tree. Pure nav logic in FolderPicker with 3 new tests (34 total green).
This commit is contained in:
parent
3e0e3b50cb
commit
b45e9ff216
2 changed files with 143 additions and 11 deletions
|
|
@ -26,6 +26,7 @@ import androidx.compose.material3.ButtonDefaults
|
|||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -43,7 +44,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import java.io.File
|
||||
import javax.swing.JFileChooser
|
||||
|
||||
// ---- engine -----------------------------------------------------------------
|
||||
|
||||
|
|
@ -83,16 +83,106 @@ fun EngineScreen(state: DesktopState) {
|
|||
|
||||
// ---- library ---------------------------------------------------------------
|
||||
|
||||
private fun chooseFolder(): File? {
|
||||
val chooser = JFileChooser().apply {
|
||||
dialogTitle = "Choose your recordings folder"
|
||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
isAcceptAllFileFilterUsed = false
|
||||
// Without an explicit size the dialog comes up tiny on HiDPI
|
||||
// compositors (and there is no resize handle), so pin a usable one.
|
||||
preferredSize = java.awt.Dimension(1040, 700)
|
||||
// ---- folder picker (in-app, themed, zoom-aware) -----------------------------
|
||||
|
||||
/**
|
||||
* Pure folder-picker navigation (unit-tested, no UI).
|
||||
* Children = non-hidden directories, sorted; parent walks the path back.
|
||||
*/
|
||||
object FolderPicker {
|
||||
fun children(dir: File): List<File> =
|
||||
dir.listFiles()?.filter {
|
||||
it.isDirectory && !it.name.startsWith(".")
|
||||
}?.sortedBy { it.name.lowercase() }.orEmpty()
|
||||
|
||||
/** Directory list starting at home, walking into [dir]'s chain if set. */
|
||||
fun pathFromHome(dir: File?, home: File): List<File> {
|
||||
val start = dir ?: return listOf(home)
|
||||
val chain = ArrayDeque<File>()
|
||||
var cur: File? = start
|
||||
while (cur != null) {
|
||||
chain.addFirst(cur)
|
||||
if (cur.absolutePath == home.absolutePath) break
|
||||
cur = cur.parentFile
|
||||
}
|
||||
return if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) chooser.selectedFile else null
|
||||
// If start isn't under home (external volume), show its own chain.
|
||||
return chain.toList().ifEmpty { listOf(start) }
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.compose.runtime.Composable
|
||||
fun FolderPickerDialog(
|
||||
startDir: File?,
|
||||
onResult: (File?) -> Unit,
|
||||
) {
|
||||
val home = remember { File(System.getProperty("user.home")) }
|
||||
var current by remember {
|
||||
mutableStateOf(startDir?.takeIf { it.isDirectory } ?: home)
|
||||
}
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
val subs = remember(current) { FolderPicker.children(current) }
|
||||
val crumbs = remember(current) { FolderPicker.pathFromHome(current, home) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onResult(null) },
|
||||
title = { Text("Choose your recordings folder") },
|
||||
text = {
|
||||
Column(Modifier.fillMaxWidth().heightIn(min = 320.dp, max = 460.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
// Breadcrumb row: home + each step; click to jump.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextButton({ current = home }) { Text("Home") }
|
||||
crumbs.dropWhile { it.absolutePath == home.absolutePath }
|
||||
.forEach { d ->
|
||||
Text("/", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
TextButton({ current = d }) {
|
||||
Text(d.name.ifEmpty { d.absolutePath }, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(current.absolutePath,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
HorizontalDivider()
|
||||
if (current.parentFile != null) {
|
||||
TextButton(
|
||||
{ current.parentFile?.let { current = it } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("⋱ .. (go up)") }
|
||||
}
|
||||
if (subs.isEmpty()) {
|
||||
Text("No subfolders here.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
items(subs, key = { it.absolutePath }) { d ->
|
||||
Card(Modifier.fillMaxWidth().clickable { current = d }) {
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp,
|
||||
vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("▸", color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(d.name, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton({
|
||||
if (current.isDirectory) onResult(current)
|
||||
else error = "That folder no longer exists."
|
||||
}) { Text("Choose “${current.name.ifEmpty { current.absolutePath }}”") }
|
||||
},
|
||||
dismissButton = { TextButton({ onResult(null) }) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
|
||||
@androidx.compose.runtime.Composable
|
||||
|
|
@ -103,6 +193,7 @@ fun LibraryScreen(state: DesktopState) {
|
|||
val hits by state.searchResults.collectAsState()
|
||||
val connected by state.connected.collectAsState()
|
||||
var renaming by remember { mutableStateOf<File?>(null) }
|
||||
var showPicker by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf<File?>(null) }
|
||||
var deletingMany by remember { mutableStateOf(false) }
|
||||
var deleteNotice by remember { mutableStateOf<String?>(null) }
|
||||
|
|
@ -136,7 +227,7 @@ fun LibraryScreen(state: DesktopState) {
|
|||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OutlinedButton({ chooseFolder()?.let { state.pickFolder(it) } }) {
|
||||
OutlinedButton({ showPicker = true }) {
|
||||
Text("Choose folder…")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
|
@ -300,6 +391,16 @@ fun LibraryScreen(state: DesktopState) {
|
|||
}
|
||||
}
|
||||
|
||||
if (showPicker) {
|
||||
FolderPickerDialog(
|
||||
startDir = folder ?: File(System.getProperty("user.home")),
|
||||
onResult = { picked ->
|
||||
showPicker = false
|
||||
picked?.let { state.pickFolder(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
renaming?.let { target ->
|
||||
var text by remember(target) { mutableStateOf(target.nameWithoutExtension) }
|
||||
var error by remember(target) { mutableStateOf<String?>(null) }
|
||||
|
|
|
|||
|
|
@ -94,3 +94,34 @@ class LibraryQueueTest {
|
|||
assertEquals(listOf("fresh.m4a"), picked.map { it.name })
|
||||
}
|
||||
}
|
||||
|
||||
class FolderPickerTest {
|
||||
@Test fun childrenListsOnlyVisibleDirsSorted() {
|
||||
val dir = createTempDirectory("fp").toFile()
|
||||
val b = File(dir, "Bee").apply { mkdir() }
|
||||
val a = File(dir, "apple").apply { mkdir() }
|
||||
File(dir, ".cache").apply { mkdir() }
|
||||
File(dir, "file.txt").writeText("x")
|
||||
assertEquals(listOf(a, b), FolderPicker.children(dir))
|
||||
}
|
||||
|
||||
@Test fun pathFromHomeWalksChainUnderHome() {
|
||||
val home = File("/home/x")
|
||||
val deep = File("/home/x/Music/Recordings")
|
||||
assertEquals(
|
||||
listOf(File("/home/x"), File("/home/x/Music"), deep),
|
||||
FolderPicker.pathFromHome(deep, home),
|
||||
)
|
||||
assertEquals(listOf(home), FolderPicker.pathFromHome(null, home))
|
||||
assertEquals(listOf(home), FolderPicker.pathFromHome(home, home))
|
||||
}
|
||||
|
||||
@Test fun pathFromHomeHandlesExternalVolume() {
|
||||
val home = File("/home/x")
|
||||
val ext = File("/mnt/vids/clips")
|
||||
// Not under home: its own chain, rooted at '/', no crash.
|
||||
val chain = FolderPicker.pathFromHome(ext, home)
|
||||
assertEquals(ext, chain.last())
|
||||
assertTrue(chain.size >= 3)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue