diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index 8704af95c..c9597b386 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -463,7 +463,10 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( } else items drivesWorking.clear() - val drivesStr = c?.getDrives() ?: Container.DEFAULT_DRIVES + val drivesStr = c?.let { + com.winlator.cmod.runtime.wine.WineUtils.readDrivesFromPrefix(it) + .ifEmpty { it.getDrives() } + } ?: Container.DEFAULT_DRIVES for (drive in Container.drivesIterator(drivesStr)) { drivesWorking.add( DriveDraft( @@ -848,6 +851,10 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( c.setEmulator64(emulator64) c.setWinComponents(wincomponents) c.setDrives(drivesString) + // an existing prefix owns the drive list, so write it there too + if (java.io.File(c.rootDir, ".wine/dosdevices").isDirectory) { + com.winlator.cmod.runtime.wine.WineUtils.applyDrivesToPrefix(c, drivesString) + } c.setFullscreenStretched(state.fullscreenStretched.value) c.setUseUnixLibs(state.useUnixLibs.value) c.setInputType(finalInputType) diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 9919ec9cb..899617cc9 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -15,6 +15,8 @@ Seleccionar EXE Seleccionar carpeta Carpeta actual + Ingresar ruta + Ruta no encontrada Selecciona un archivo .exe válido Selecciona un archivo .ico o .png válido. Salir diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 7c2bc7ae8..0afcb6890 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -128,6 +128,8 @@ Vælg Vælg mappe Nuværende mappe + Indtast sti + Stien blev ikke fundet Lagringsrødder Gennemse lokale mapper direkte Ingen mapper tilgængelige her diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 303dbfa8e..ffc75f99e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -128,6 +128,8 @@ Auswählen Ordner auswählen Aktueller Ordner + Pfad eingeben + Pfad nicht gefunden Speicherorte Lokale Ordner direkt durchsuchen Hier sind keine Ordner verfügbar diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5cc622090..40ce3eb87 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -128,6 +128,8 @@ Seleccionar Seleccionar carpeta Carpeta actual + Introducir ruta + Ruta no encontrada Ubicaciones de almacenamiento Explorar carpetas locales directamente No hay carpetas disponibles aquí diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index f62d0a67c..cc948e9f1 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -15,6 +15,8 @@ Valitse EXE Valitse kansio Nykyinen kansio + Anna polku + Polkua ei löytynyt Valitse kelvollinen .exe-tiedosto Valitse kelvollinen .ico- tai .png-tiedosto. Poistu diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e8865ddbc..cc6348beb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -128,6 +128,8 @@ Sélectionner Sélectionner un dossier Dossier actuel + Saisir le chemin + Chemin introuvable Emplacements de stockage Parcourir directement les dossiers locaux Aucun dossier disponible ici diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 63edff4cd..1c318e812 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -15,6 +15,8 @@ EXE चुनें फ़ोल्डर चुनें वर्तमान फ़ोल्डर + पथ दर्ज करें + पथ नहीं मिला कृपया मान्य .exe फ़ाइल चुनें कृपया मान्य .ico या .png फ़ाइल चुनें। बाहर निकलें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 99e2cf56c..aa3d9ce0b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -128,6 +128,8 @@ Seleziona Seleziona cartella Cartella corrente + Inserisci percorso + Percorso non trovato Percorsi di archiviazione Sfoglia direttamente le cartelle locali Nessuna cartella disponibile qui diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f444592f9..f1dede4f0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,6 +15,8 @@ EXEを選択 フォルダを選択 現在のフォルダ + パスを入力 + パスが見つかりません 有効な.exeファイルを選択してください 有効な.icoまたは.pngファイルを選択してください。 終了 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3d9dd0d1c..65706aba0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -128,6 +128,8 @@ 선택 폴더 선택 현재 폴더 + 경로 입력 + 경로를 찾을 수 없음 저장소 루트 로컬 폴더를 바로 탐색 여기에서 사용할 수 있는 폴더가 없습니다 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 144ab9a8e..e6ee7acf6 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -15,6 +15,8 @@ Velg EXE Velg mappe Gjeldende mappe + Angi sti + Fant ikke stien Velg en gyldig .exe-fil Velg en gyldig .ico- eller .png-fil. Avslutt diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 33f7e49df..402e17c95 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -128,6 +128,8 @@ Wybierz Wybierz folder Bieżący folder + Wprowadź ścieżkę + Nie znaleziono ścieżki Lokalizacje pamięci Przeglądaj lokalne foldery bezpośrednio Brak dostępnych folderów w tej lokalizacji diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 144328d65..8dc46b6ae 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -128,6 +128,8 @@ Selecionar Selecionar pasta Pasta atual + Inserir caminho + Caminho não encontrado Locais de armazenamento Navegar pelas pastas locais diretamente Nenhuma pasta disponível aqui diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 36753863d..ed79dea01 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -15,6 +15,8 @@ Selecionar EXE Selecionar Pasta Pasta Atual + Introduzir caminho + Caminho não encontrado Selecione um ficheiro .exe válido Selecione um ficheiro .ico ou .png válido. Sair diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2577a0474..a4a1f22cc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -128,6 +128,8 @@ Selectează Selectează folder Folder curent + Introduceți calea + Calea nu a fost găsită Locații de stocare Răsfoiește direct folderele locale Niciun folder disponibil aici diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d63098f20..b11a7a883 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,6 +15,8 @@ Выбрать EXE Выбрать папку Текущая папка + Введите путь + Путь не найден Выберите корректный файл .exe Выберите корректный файл .ico или .png. Выход diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5a9dfd660..e57383056 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -15,6 +15,8 @@ Välj EXE Välj mapp Aktuell mapp + Ange sökväg + Sökvägen hittades inte Välj en giltig .exe-fil Välj en giltig .ico- eller .png-fil. Avsluta diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 048f4e759..19e33d9f4 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -15,6 +15,8 @@ เลือก EXE เลือกโฟลเดอร์ โฟลเดอร์ปัจจุบัน + ป้อนพาธ + ไม่พบพาธ โปรดเลือกไฟล์ .exe ที่ถูกต้อง โปรดเลือกไฟล์ .ico หรือ .png ที่ถูกต้อง ออก diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index acd1fe4f0..9d3407a01 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -15,6 +15,8 @@ EXE Seç Klasör Seç Geçerli Klasör + Yol girin + Yol bulunamadı Lütfen geçerli bir .exe dosyası seçin Lütfen geçerli bir .ico veya .png dosyası seçin. Çıkış diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index aa2251751..b6dcc341c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -128,6 +128,8 @@ Вибрати Вибрати теку Поточна тека + Введіть шлях + Шлях не знайдено Сховища Переглядати локальні теки безпосередньо Тут немає доступних тек diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fb359c30b..59d94be2b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -128,6 +128,8 @@ 选择 选择文件夹 当前文件夹 + 输入路径 + 找不到路径 存储根目录 直接浏览本地文件夹 此处没有可用的文件夹 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6312633aa..6b75c25c0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -128,6 +128,8 @@ 選擇 選擇資料夾 目前資料夾 + 輸入路徑 + 找不到路徑 儲存根目錄 直接瀏覽本機資料夾 這裡沒有可用的資料夾 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 364f9c208..aa83bcf14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ Select EXE Select Folder Current Folder + Enter Path + Path not found Please select a valid .exe file Please select a valid .ico or .png file. Exit diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index b5429ae71..8f0fa1e77 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -6803,7 +6803,8 @@ private void setupXEnvironment() throws PackageManager.NameNotFoundException { normalizeSyncEnvVars(envVars); ArrayList bindingPaths = new ArrayList<>(); - String drives = shortcut != null ? getShortcutSetting("drives", container.getDrives()) : container.getDrives(); + String drives = WineUtils.readDrivesFromPrefix(container); + if (drives.isEmpty()) drives = container.getDrives(); for (String[] drive : Container.drivesIterator(drives)) { bindingPaths.add(drive[1]); } diff --git a/app/src/main/runtime/wine/WineUtils.java b/app/src/main/runtime/wine/WineUtils.java index a64996e82..994336d9c 100644 --- a/app/src/main/runtime/wine/WineUtils.java +++ b/app/src/main/runtime/wine/WineUtils.java @@ -17,6 +17,7 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -60,10 +61,13 @@ public static String hostPathToMappedWinePath( String bestDriveLetter = null; String bestDriveRoot = null; - String drives = - container != null && container.getDrives() != null - ? container.getDrives() - : Container.DEFAULT_DRIVES; + String drives = readDrivesFromPrefix(container); + if (drives.isEmpty()) { + drives = + container != null && container.getDrives() != null + ? container.getDrives() + : Container.DEFAULT_DRIVES; + } for (String[] drive : Container.drivesIterator(drives)) { if (drive.length < 2) continue; @@ -375,50 +379,57 @@ private static boolean isSupportedDriveLetter(String letter) { return letter.length() == 1 && Character.isLetter(letter.charAt(0)); } - public static void createDosdevicesSymlinks( - Container container, @Nullable String gameDirectoryPath, boolean exposeSteamGameLink) { - Log.d( - "ContainerLaunch", - "createDosdevicesSymlinks: rootDir=" - + container.getRootDir().getAbsolutePath() - + " drives=" - + container.getDrives() - + " gameDir=" - + gameDirectoryPath); + // dosdevices is the source of truth for drives; c: and z: stay app-managed + public static String readDrivesFromPrefix(@Nullable Container container) { + if (container == null) return ""; File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); - if (!dosdevicesDir.exists()) { - boolean created = dosdevicesDir.mkdirs(); - Log.d("ContainerLaunch", "createDosdevicesSymlinks: created dosdevices dir=" + created); - } - String dosdevicesPath = dosdevicesDir.getPath(); File[] files = dosdevicesDir.listFiles(); - if (files != null) for (File file : files) if (file.getName().matches("[a-z]:")) file.delete(); + if (files == null) return ""; + + List names = new ArrayList<>(); + for (File file : files) if (file.getName().matches("[a-z]:")) names.add(file.getName()); + Collections.sort(names); + + StringBuilder drives = new StringBuilder(); + for (String name : names) { + if (name.equals("c:") || name.equals("z:")) continue; + String target = FileUtils.readSymlink(new File(dosdevicesDir, name)); + if (target.isEmpty()) continue; + + String path = target; + if (!new File(target).isAbsolute()) { + File resolved = new File(dosdevicesDir, target); + try { + path = resolved.getCanonicalPath(); + } catch (IOException e) { + path = resolved.getAbsolutePath(); + } + } + if (path.isEmpty() || path.contains(":")) continue; + drives.append(name.substring(0, 1).toUpperCase(Locale.ENGLISH)).append(':').append(path); + } + return drives.toString(); + } - FileUtils.symlink("../drive_c", dosdevicesPath + "/c:"); - FileUtils.symlink(container.getRootDir().getPath() + "/../..", dosdevicesPath + "/z:"); + public static void applyDrivesToPrefix(@Nullable Container container, @Nullable String drives) { + if (container == null) return; + File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); + if (!dosdevicesDir.exists()) dosdevicesDir.mkdirs(); + String dosdevicesPath = dosdevicesDir.getPath(); String packageStorageSuffix = "/com.winnative.cmod/storage"; String legacyPackageStorageSuffix = "/com.winlator.cmod/storage"; - Context context = null; if (container.getManager() != null && container.getManager().getContext() != null) { - context = container.getManager().getContext(); - String packageName = context.getPackageName(); - packageStorageSuffix = "/" + packageName + "/storage"; + packageStorageSuffix = + "/" + container.getManager().getContext().getPackageName() + "/storage"; } - if (context != null) { - String normalizedDrives = normalizePersistentDrives(context, container.getDrives(), false); - if (normalizedDrives != null - && !normalizedDrives.isEmpty() - && !normalizedDrives.equals(container.getDrives())) { - container.setDrives(normalizedDrives); - Log.d("WineUtils", "Normalized launch drives in memory to: " + normalizedDrives); - } - } + LinkedHashSet applied = new LinkedHashSet<>(); + for (String[] drive : Container.drivesIterator(drives != null ? drives : "")) { + if (drive.length < 2 || "A".equalsIgnoreCase(drive[0])) continue; + String letter = drive[0].toLowerCase(Locale.ENGLISH); + if (letter.equals("c") || letter.equals("z")) continue; - int driveCount = 0; - for (String[] drive : container.drivesIterator()) { - if ("A".equalsIgnoreCase(drive[0])) continue; String path = resolveReadableDrivePath(drive[1]); File linkTarget = new File(path); path = linkTarget.getAbsolutePath(); @@ -428,11 +439,44 @@ public static void createDosdevicesSymlinks( linkTarget.mkdirs(); FileUtils.chmod(linkTarget, 0771); } - FileUtils.symlink(path, dosdevicesPath + "/" + drive[0].toLowerCase(Locale.ENGLISH) + ":"); - Log.d("ContainerLaunch", "createDosdevicesSymlinks: " + drive[0] + ": -> " + path); - driveCount++; + FileUtils.symlink(path, dosdevicesPath + "/" + letter + ":"); + applied.add(letter + ":"); + Log.d("ContainerLaunch", "applyDrivesToPrefix: " + letter + ": -> " + path); } - Log.d("ContainerLaunch", "createDosdevicesSymlinks: created " + driveCount + " drive symlinks"); + + File[] files = dosdevicesDir.listFiles(); + if (files != null) + for (File file : files) { + String name = file.getName(); + if (!name.matches("[a-z]:") || name.equals("c:") || name.equals("z:")) continue; + if (!applied.contains(name)) file.delete(); + } + } + + public static void createDosdevicesSymlinks( + Container container, @Nullable String gameDirectoryPath, boolean exposeSteamGameLink) { + File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); + if (!dosdevicesDir.exists()) dosdevicesDir.mkdirs(); + String dosdevicesPath = dosdevicesDir.getPath(); + + FileUtils.symlink("../drive_c", dosdevicesPath + "/c:"); + FileUtils.symlink(container.getRootDir().getPath() + "/../..", dosdevicesPath + "/z:"); + + // the container config only seeds a prefix that has no drives yet + String drives = readDrivesFromPrefix(container); + Log.d("ContainerLaunch", "createDosdevicesSymlinks: entries=" + Arrays.toString(dosdevicesDir.list()) + " read=" + drives); + if (drives.isEmpty()) { + drives = container.getDrives(); + Context context = + container.getManager() != null ? container.getManager().getContext() : null; + if (context != null) { + String normalizedDrives = normalizePersistentDrives(context, drives, false); + if (normalizedDrives != null && !normalizedDrives.isEmpty()) drives = normalizedDrives; + } + Log.d("ContainerLaunch", "createDosdevicesSymlinks: seeding fresh prefix with " + drives); + } + + applyDrivesToPrefix(container, drives); // Only expose Steam's steamapps/common symlink for actual Steam launches. if (gameDirectoryPath != null && !gameDirectoryPath.isEmpty()) { diff --git a/app/src/main/shared/android/DirectoryPickerDialog.kt b/app/src/main/shared/android/DirectoryPickerDialog.kt index 25986ac6f..6e41f99f2 100644 --- a/app/src/main/shared/android/DirectoryPickerDialog.kt +++ b/app/src/main/shared/android/DirectoryPickerDialog.kt @@ -75,6 +75,9 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.OutlinedTextField +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.material3.ripple import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -419,6 +422,8 @@ object DirectoryPickerDialog { val scope = rememberCoroutineScope() var currentDir by remember(initialDir.absolutePath) { mutableStateOf(initialDir) } var selectedFile by remember { mutableStateOf(null) } + var showPathInput by remember { mutableStateOf(false) } + var pathInputText by remember { mutableStateOf("") } var rootsExpanded by remember { mutableStateOf(false) } var refreshTick by remember { mutableStateOf(0) } var clipboard by remember { mutableStateOf(null) } @@ -731,7 +736,7 @@ object DirectoryPickerDialog { footerZone = false } LaunchedEffect(menuTarget) { if (menuTarget != null) menuRegistry.reset() } - LaunchedEffect(renameTarget, showNewFolder, deleteTargets, runTarget, transferProgress != null) { + LaunchedEffect(renameTarget, showNewFolder, showPathInput, deleteTargets, runTarget, transferProgress != null) { overlayRegistry.controllerActive = false overlayRegistry.reset() } @@ -753,11 +758,28 @@ object DirectoryPickerDialog { footerRegistry.controllerActive = false contentRegistry.controllerActive = true } + val pathNotFound = activityString(R.string.common_ui_path_not_found) + + fun applyTypedPath(typed: String) { + val target = File(typed.trim()) + if (target.isDirectory) { + currentDir = target + selectedFile = null + } else if (target.isFile) { + currentDir = target.parentFile ?: currentDir + selectedFile = target + } else { + Toast.makeText(context, pathNotFound, Toast.LENGTH_SHORT).show() + } + showPathInput = false + } + val handlers = remember(window) { paneNavHandlers( onDismiss = { when { + showPathInput -> showPathInput = false renameTarget != null -> renameTarget = null showNewFolder -> showNewFolder = false deleteTargets != null -> deleteTargets = null @@ -775,9 +797,11 @@ object DirectoryPickerDialog { onStart = { val overlayOpen = rootsExpanded || menuTarget != null || renameTarget != null || - showNewFolder || deleteTargets != null || runTarget != null || - transferProgress != null - if (!overlayOpen) { + showNewFolder || showPathInput || deleteTargets != null || + runTarget != null || transferProgress != null + if (showPathInput) { + applyTypedPath(pathInputText) + } else if (!overlayOpen) { if (manage) { if (clipboard != null) pasteInto(currentDir) else onDismiss() } else { @@ -791,8 +815,9 @@ object DirectoryPickerDialog { when { menuTarget != null -> menuRegistry rootsExpanded -> rootsRegistry - renameTarget != null || showNewFolder || deleteTargets != null || - runTarget != null || transferProgress != null -> overlayRegistry + renameTarget != null || showNewFolder || showPathInput || + deleteTargets != null || runTarget != null || + transferProgress != null -> overlayRegistry footerZone -> footerRegistry else -> contentRegistry } @@ -898,7 +923,13 @@ object DirectoryPickerDialog { } Spacer(Modifier.height(4.dp)) - CurrentPathCard(path = selectedFile?.name ?: currentDir.absolutePath) + CurrentPathCard( + path = selectedFile?.name ?: currentDir.absolutePath, + onClick = { + pathInputText = currentDir.absolutePath + showPathInput = true + }, + ) Spacer(Modifier.height(8.dp)) androidx.compose.foundation.layout.Box( @@ -1186,6 +1217,20 @@ object DirectoryPickerDialog { } } } + + if (showPathInput) { + CompositionLocalProvider(LocalPaneNav provides overlayRegistry) { + TextInputOverlay( + modifier = Modifier.matchParentSize(), + title = activityString(R.string.common_ui_enter_path), + initial = pathInputText, + confirmLabel = activityString(R.string.common_ui_ok), + onTextChange = { pathInputText = it }, + onConfirm = { applyTypedPath(it) }, + onDismiss = { showPathInput = false }, + ) + } + } } } @@ -1236,15 +1281,23 @@ object DirectoryPickerDialog { } @Composable - private fun CurrentPathCard(path: String) { + private fun CurrentPathCard( + path: String, + onClick: () -> Unit, + ) { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) + .paneNavItem(cornerRadius = 12.dp, onActivate = onClick) .background(BgDark) .border(1.dp, CardBorder, RoundedCornerShape(12.dp)) - .padding(horizontal = CurrentPathHorizontalPadding, vertical = 4.dp), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ).padding(horizontal = CurrentPathHorizontalPadding, vertical = 4.dp), contentAlignment = Alignment.CenterStart, ) { Text( @@ -2013,8 +2066,15 @@ object DirectoryPickerDialog { confirmLabel: String, onConfirm: (String) -> Unit, onDismiss: () -> Unit, + onTextChange: (String) -> Unit = {}, ) { var text by remember { mutableStateOf(initial) } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboard?.show() + } Box( modifier = modifier @@ -2048,9 +2108,12 @@ object DirectoryPickerDialog { Spacer(Modifier.height(12.dp)) OutlinedTextField( value = text, - onValueChange = { text = it }, + onValueChange = { + text = it + onTextChange(it) + }, singleLine = true, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), ) Spacer(Modifier.height(16.dp)) Row(