diff --git a/Common/CacheData.qml b/Common/CacheData.qml index dd4b9be8..64da9b36 100644 --- a/Common/CacheData.qml +++ b/Common/CacheData.qml @@ -22,6 +22,57 @@ Singleton { property string wallpaperLastPath: "" property string profileLastPath: "" + property var fileBrowserSettings: ({ + "wallpaper": { + "lastPath": "", + "viewMode": "grid", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + }, + "profile": { + "lastPath": "", + "viewMode": "grid", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + }, + "notepad_save": { + "lastPath": "", + "viewMode": "list", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + }, + "notepad_load": { + "lastPath": "", + "viewMode": "list", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + }, + "generic": { + "lastPath": "", + "viewMode": "list", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + }, + "default": { + "lastPath": "", + "viewMode": "list", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + } + }) + Component.onCompleted: { if (!isGreeterMode) { loadCache() @@ -43,6 +94,37 @@ Singleton { wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "" profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "" + if (cache.fileBrowserSettings !== undefined) { + fileBrowserSettings = cache.fileBrowserSettings + } else if (cache.fileBrowserViewMode !== undefined) { + fileBrowserSettings = { + "wallpaper": { + "lastPath": cache.wallpaperLastPath || "", + "viewMode": cache.fileBrowserViewMode || "grid", + "sortBy": cache.fileBrowserSortBy || "name", + "sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true, + "iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1, + "showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true + }, + "profile": { + "lastPath": cache.profileLastPath || "", + "viewMode": cache.fileBrowserViewMode || "grid", + "sortBy": cache.fileBrowserSortBy || "name", + "sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true, + "iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1, + "showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true + }, + "file": { + "lastPath": "", + "viewMode": "list", + "sortBy": "name", + "sortAscending": true, + "iconSizeIndex": 1, + "showSidebar": true + } + } + } + if (cache.configVersion === undefined) { migrateFromUndefinedToV1(cache) cleanupUnusedKeys() @@ -62,6 +144,7 @@ Singleton { cacheFile.setText(JSON.stringify({ "wallpaperLastPath": wallpaperLastPath, "profileLastPath": profileLastPath, + "fileBrowserSettings": fileBrowserSettings, "configVersion": cacheConfigVersion }, null, 2)) } @@ -74,6 +157,7 @@ Singleton { const validKeys = [ "wallpaperLastPath", "profileLastPath", + "fileBrowserSettings", "configVersion" ] diff --git a/Modals/FileBrowser/FileBrowserGridDelegate.qml b/Modals/FileBrowser/FileBrowserGridDelegate.qml new file mode 100644 index 00000000..42a6fb24 --- /dev/null +++ b/Modals/FileBrowser/FileBrowserGridDelegate.qml @@ -0,0 +1,214 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Widgets + +StyledRect { + id: delegateRoot + + required property bool fileIsDir + required property string filePath + required property string fileName + required property int index + + property bool weMode: false + property var iconSizes: [80, 120, 160, 200] + property int iconSizeIndex: 1 + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + + signal itemClicked(int index, string path, string name, bool isDir) + signal itemSelected(int index, string path, string name, bool isDir) + + function isImageFile(fileName) { + if (!fileName) { + return false + } + const ext = fileName.toLowerCase().split('.').pop() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext) + } + + function getFileIcon(fileName, isDir) { + if (isDir) { + return "folder" + } + if (!fileName) { + return "description" + } + const ext = fileName.toLowerCase().split('.').pop() + const iconMap = { + "mp3": 'music_note', + "wav": 'music_note', + "flac": 'music_note', + "ogg": 'music_note', + "aac": 'music_note', + "mp4": 'movie', + "mkv": 'movie', + "avi": 'movie', + "mov": 'movie', + "webm": 'movie', + "flv": 'movie', + "wmv": 'movie', + "jpg": 'image', + "jpeg": 'image', + "png": 'image', + "gif": 'image', + "bmp": 'image', + "webp": 'image', + "svg": 'image', + "pdf": 'picture_as_pdf', + "zip": 'folder_zip', + "rar": 'folder_zip', + "7z": 'folder_zip', + "tar": 'folder_zip', + "gz": 'folder_zip', + "bz2": 'folder_zip', + "xz": 'folder_zip', + "txt": 'description', + "md": 'description', + "doc": 'description', + "docx": 'description', + "odt": 'description', + "rtf": 'description', + "sh": 'terminal', + "py": 'code', + "js": 'code', + "ts": 'code', + "cpp": 'code', + "c": 'code', + "h": 'code', + "java": 'code', + "go": 'code', + "rs": 'code', + "php": 'code', + "rb": 'code', + "qml": 'code', + "html": 'code', + "css": 'code', + "json": 'data_object', + "xml": 'data_object', + "yaml": 'data_object', + "yml": 'data_object', + "toml": 'data_object' + } + return iconMap[ext] || 'description' + } + + width: weMode ? 245 : iconSizes[iconSizeIndex] + 16 + height: weMode ? 205 : iconSizes[iconSizeIndex] + 48 + radius: Theme.cornerRadius + color: { + if (keyboardNavigationActive && delegateRoot.index === selectedIndex) + return Theme.surfacePressed + + return mouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent" + } + border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent" + border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0 + + Component.onCompleted: { + if (keyboardNavigationActive && delegateRoot.index === selectedIndex) + itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + } + + onSelectedIndexChanged: { + if (keyboardNavigationActive && selectedIndex === delegateRoot.index) + itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + } + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + Item { + width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8) + height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8) + anchors.horizontalCenter: parent.horizontalCenter + + CachingImage { + id: gridPreviewImage + anchors.fill: parent + anchors.margins: 2 + property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"] + property int weExtIndex: 0 + source: { + if (weMode && delegateRoot.fileIsDir) { + return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex] + } + return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : "" + } + onStatusChanged: { + if (weMode && delegateRoot.fileIsDir && status === Image.Error) { + if (weExtIndex < weExtensions.length - 1) { + weExtIndex++ + source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex] + } else { + source = "" + } + } + } + fillMode: Image.PreserveAspectCrop + maxCacheSize: weMode ? 225 : iconSizes[iconSizeIndex] + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: gridPreviewImage + maskEnabled: true + maskSource: gridImageMask + visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)) + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: gridImageMask + anchors.fill: parent + anchors.margins: 2 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: "black" + antialiasing: true + } + } + + DankIcon { + anchors.centerIn: parent + name: getFileIcon(delegateRoot.fileName, delegateRoot.fileIsDir) + size: iconSizes[iconSizeIndex] * 0.45 + color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText + visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode) + } + } + + StyledText { + text: delegateRoot.fileName || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: delegateRoot.width - Theme.spacingM + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + maximumLineCount: 2 + wrapMode: Text.Wrap + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + } + } +} diff --git a/Modals/FileBrowser/FileBrowserListDelegate.qml b/Modals/FileBrowser/FileBrowserListDelegate.qml new file mode 100644 index 00000000..b7610253 --- /dev/null +++ b/Modals/FileBrowser/FileBrowserListDelegate.qml @@ -0,0 +1,219 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Widgets + +StyledRect { + id: listDelegateRoot + + required property bool fileIsDir + required property string filePath + required property string fileName + required property int index + required property var fileModified + required property int fileSize + + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + + signal itemClicked(int index, string path, string name, bool isDir) + signal itemSelected(int index, string path, string name, bool isDir) + + function isImageFile(fileName) { + if (!fileName) { + return false + } + const ext = fileName.toLowerCase().split('.').pop() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext) + } + + function getFileIcon(fileName, isDir) { + if (isDir) { + return "folder" + } + if (!fileName) { + return "description" + } + const ext = fileName.toLowerCase().split('.').pop() + const iconMap = { + "mp3": 'music_note', + "wav": 'music_note', + "flac": 'music_note', + "ogg": 'music_note', + "aac": 'music_note', + "mp4": 'movie', + "mkv": 'movie', + "avi": 'movie', + "mov": 'movie', + "webm": 'movie', + "flv": 'movie', + "wmv": 'movie', + "jpg": 'image', + "jpeg": 'image', + "png": 'image', + "gif": 'image', + "bmp": 'image', + "webp": 'image', + "svg": 'image', + "pdf": 'picture_as_pdf', + "zip": 'folder_zip', + "rar": 'folder_zip', + "7z": 'folder_zip', + "tar": 'folder_zip', + "gz": 'folder_zip', + "bz2": 'folder_zip', + "xz": 'folder_zip', + "txt": 'description', + "md": 'description', + "doc": 'description', + "docx": 'description', + "odt": 'description', + "rtf": 'description', + "sh": 'terminal', + "py": 'code', + "js": 'code', + "ts": 'code', + "cpp": 'code', + "c": 'code', + "h": 'code', + "java": 'code', + "go": 'code', + "rs": 'code', + "php": 'code', + "rb": 'code', + "qml": 'code', + "html": 'code', + "css": 'code', + "json": 'data_object', + "xml": 'data_object', + "yaml": 'data_object', + "yml": 'data_object', + "toml": 'data_object' + } + return iconMap[ext] || 'description' + } + + function formatFileSize(size) { + if (size < 1024) + return size + " B" + if (size < 1024 * 1024) + return (size / 1024).toFixed(1) + " KB" + if (size < 1024 * 1024 * 1024) + return (size / (1024 * 1024)).toFixed(1) + " MB" + return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB" + } + + height: 44 + radius: Theme.cornerRadius + color: { + if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) + return Theme.surfacePressed + return listMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent" + } + border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent" + border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0 + + Component.onCompleted: { + if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) + itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) + } + + onSelectedIndexChanged: { + if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index) + itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) + } + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + Item { + width: 28 + height: 28 + anchors.verticalCenter: parent.verticalCenter + + CachingImage { + id: listPreviewImage + anchors.fill: parent + source: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? ("file://" + listDelegateRoot.filePath) : "" + fillMode: Image.PreserveAspectCrop + maxCacheSize: 32 + visible: false + } + + MultiEffect { + anchors.fill: parent + source: listPreviewImage + maskEnabled: true + maskSource: listImageMask + visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName) + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: listImageMask + anchors.fill: parent + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: "black" + antialiasing: true + } + } + + DankIcon { + anchors.centerIn: parent + name: getFileIcon(listDelegateRoot.fileName, listDelegateRoot.fileIsDir) + size: Theme.iconSize - 2 + color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText + visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName) + } + } + + StyledText { + text: listDelegateRoot.fileName || "" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width - 280 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + maximumLineCount: 1 + clip: true + } + + StyledText { + text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: 70 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: 140 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: listMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) + } + } +} diff --git a/Modals/FileBrowser/FileBrowserModal.qml b/Modals/FileBrowser/FileBrowserModal.qml index d5e8df88..02f69764 100644 --- a/Modals/FileBrowser/FileBrowserModal.qml +++ b/Modals/FileBrowser/FileBrowserModal.qml @@ -5,6 +5,7 @@ import QtQuick.Controls import Quickshell.Io import qs.Common import qs.Modals.Common +import qs.Modals.FileBrowser import qs.Widgets DankModal { @@ -16,13 +17,13 @@ DankModal { property alias filterExtensions: fileBrowserModal.fileExtensions property string browserTitle: "Select File" property string browserIcon: "folder_open" - property string browserType: "generic" // "wallpaper" or "profile" for last path memory + property string browserType: "generic" property bool showHiddenFiles: false property int selectedIndex: -1 property bool keyboardNavigationActive: false property bool backButtonFocused: false - property bool saveMode: false // Enable save functionality - property string defaultFileName: "" // Default filename for save mode + property bool saveMode: false + property string defaultFileName: "" property int keyboardSelectionIndex: -1 property bool keyboardSelectionRequested: false property bool showKeyboardHints: false @@ -36,9 +37,67 @@ DankModal { property string wePath: "" property bool weMode: false property var parentModal: null + property bool showSidebar: true + property string viewMode: "grid" + property string sortBy: "name" + property bool sortAscending: true + property int iconSizeIndex: 1 + property var iconSizes: [80, 120, 160, 200] + property bool pathEditMode: false + property bool pathInputHasFocus: false + property int actualGridColumns: 5 + property bool _initialized: false signal fileSelected(string path) + function loadSettings() { + const type = browserType || "default" + const settings = CacheData.fileBrowserSettings[type] + const isImageBrowser = ["wallpaper", "profile"].includes(browserType) + + if (settings) { + viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list") + sortBy = settings.sortBy || "name" + sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true + iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1 + showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true + } else { + viewMode = isImageBrowser ? "grid" : "list" + } + } + + function saveSettings() { + if (!_initialized) + return + + const type = browserType || "default" + let settings = CacheData.fileBrowserSettings + if (!settings[type]) { + settings[type] = {} + } + settings[type].viewMode = viewMode + settings[type].sortBy = sortBy + settings[type].sortAscending = sortAscending + settings[type].iconSizeIndex = iconSizeIndex + settings[type].showSidebar = showSidebar + settings[type].lastPath = currentPath + CacheData.fileBrowserSettings = settings + + if (browserType === "wallpaper") { + CacheData.wallpaperLastPath = currentPath + } else if (browserType === "profile") { + CacheData.profileLastPath = currentPath + } + + CacheData.saveCache() + } + + onViewModeChanged: saveSettings() + onSortByChanged: saveSettings() + onSortAscendingChanged: saveSettings() + onIconSizeIndexChanged: saveSettings() + onShowSidebarChanged: saveSettings() + function isImageFile(fileName) { if (!fileName) { return false @@ -48,17 +107,26 @@ DankModal { } function getLastPath() { - const lastPath = browserType === "wallpaper" ? CacheData.wallpaperLastPath : browserType === "profile" ? CacheData.profileLastPath : "" + const type = browserType || "default" + const settings = CacheData.fileBrowserSettings[type] + const lastPath = settings?.lastPath || "" return (lastPath && lastPath !== "") ? lastPath : homeDir } function saveLastPath(path) { + const type = browserType || "default" + let settings = CacheData.fileBrowserSettings + if (!settings[type]) { + settings[type] = {} + } + settings[type].lastPath = path + CacheData.fileBrowserSettings = settings + CacheData.saveCache() + if (browserType === "wallpaper") { CacheData.wallpaperLastPath = path - CacheData.saveCache() } else if (browserType === "profile") { CacheData.profileLastPath = path - CacheData.saveCache() } } @@ -106,13 +174,11 @@ DankModal { } function handleSaveFile(filePath) { - // Ensure the filePath has the correct file:// protocol format var normalizedPath = filePath if (!normalizedPath.startsWith("file://")) { normalizedPath = "file://" + filePath } - // Check if file exists by looking through the folder model var exists = false var fileName = filePath.split('/').pop() @@ -137,15 +203,15 @@ DankModal { closeOnEscapeKey: false shouldHaveFocus: shouldBeVisible Component.onCompleted: { + loadSettings() currentPath = getLastPath() + _initialized = true } - property var steamPaths: [ - StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", - StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", - StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", - StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960" - ] + property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation( + StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation( + StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation( + StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"] property int currentPathIndex: 0 function discoverWallpaperEngine() { @@ -175,17 +241,17 @@ DankModal { parentModal.allowFocusOverride = true } Qt.callLater(() => { - if (contentLoader && contentLoader.item) { - contentLoader.item.forceActiveFocus() - } - }) + if (contentLoader && contentLoader.item) { + contentLoader.item.forceActiveFocus() + } + }) } onDialogClosed: { if (parentModal) { parentModal.allowFocusOverride = false parentModal.shouldHaveFocus = Qt.binding(() => { - return parentModal.shouldBeVisible - }) + return parentModal.shouldBeVisible + }) } } onVisibleChanged: { @@ -203,6 +269,7 @@ DankModal { selectedFilePath = "" selectedFileName = "" selectedFileIsDir = false + saveSettings() } onSelectedIndexChanged: { if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) { @@ -222,13 +289,58 @@ DankModal { showFiles: true showDirs: true folder: currentPath ? "file://" + currentPath : "file://" + homeDir + sortField: { + switch (sortBy) { + case "name": + return FolderListModel.Name + case "size": + return FolderListModel.Size + case "modified": + return FolderListModel.Time + case "type": + return FolderListModel.Type + default: + return FolderListModel.Name + } + } + sortReversed: !sortAscending } + property var quickAccessLocations: [{ + "name": "Home", + "path": homeDir, + "icon": "home" + }, { + "name": "Documents", + "path": homeDir + "/Documents", + "icon": "description" + }, { + "name": "Downloads", + "path": homeDir + "/Downloads", + "icon": "download" + }, { + "name": "Pictures", + "path": homeDir + "/Pictures", + "icon": "image" + }, { + "name": "Music", + "path": homeDir + "/Music", + "icon": "music_note" + }, { + "name": "Videos", + "path": homeDir + "/Videos", + "icon": "movie" + }, { + "name": "Desktop", + "path": homeDir + "/Desktop", + "icon": "computer" + }] + QtObject { id: keyboardController property int totalItems: folderModel.count - property int gridColumns: 5 + property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns) function handleKey(event) { if (event.key === Qt.Key_Escape) { @@ -236,19 +348,16 @@ DankModal { event.accepted = true return } - // F10 toggles keyboard hints if (event.key === Qt.Key_F10) { showKeyboardHints = !showKeyboardHints event.accepted = true return } - // F1 or I key for file information if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) { showFileInfo = !showFileInfo event.accepted = true return } - // Alt+Left or Backspace to go back if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) { if (currentPath !== homeDir) { navigateUp() @@ -257,10 +366,8 @@ DankModal { return } if (!keyboardNavigationActive) { - const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || - (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || - (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || - (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) + const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key + === Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) if (isInitKey) { keyboardNavigationActive = true @@ -368,6 +475,8 @@ DankModal { } break case Qt.Key_Left: + if (pathInputHasFocus) + return if (backButtonFocused) return @@ -380,6 +489,9 @@ DankModal { event.accepted = true break case Qt.Key_Right: + if (pathInputHasFocus) + return + if (backButtonFocused) { backButtonFocused = false selectedIndex = 0 @@ -391,14 +503,17 @@ DankModal { case Qt.Key_Up: if (backButtonFocused) { backButtonFocused = false - // Go to first row, appropriate column - var col = selectedIndex % gridColumns - selectedIndex = Math.min(col, totalItems - 1) + if (gridColumns === 1) { + selectedIndex = 0 + } else { + var col = selectedIndex % gridColumns + selectedIndex = Math.min(col, totalItems - 1) + } } else if (selectedIndex >= gridColumns) { - // Move up one row selectedIndex -= gridColumns + } else if (selectedIndex > 0 && gridColumns === 1) { + selectedIndex-- } else if (currentPath !== homeDir) { - // At top row, go to back button backButtonFocused = true selectedIndex = -1 } @@ -408,13 +523,15 @@ DankModal { if (backButtonFocused) { backButtonFocused = false selectedIndex = 0 + } else if (gridColumns === 1) { + if (selectedIndex < totalItems - 1) { + selectedIndex++ + } } else { - // Move down one row if possible var newIndex = selectedIndex + gridColumns if (newIndex < totalItems) { selectedIndex = newIndex } else { - // If can't go down a full row, go to last item in the column if exists var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns var col = selectedIndex % gridColumns var targetIndex = lastRowStart + col @@ -431,7 +548,6 @@ DankModal { if (backButtonFocused) navigateUp() else if (selectedIndex >= 0 && selectedIndex < totalItems) - // Trigger selection by setting the grid's current index and using signal fileBrowserModal.keyboardFileSelection(selectedIndex) event.accepted = true break @@ -446,8 +562,6 @@ DankModal { interval: 1 onTriggered: { - // Access the currently selected item through model role names - // This will work because QML models expose role data executeKeyboardSelection(targetIndex) } } @@ -459,14 +573,14 @@ DankModal { running: false onExited: exitCode => { - if (exitCode === 0) { - fileBrowserModal.weAvailable = true - fileBrowserModal.wePath = wePath - } else { - currentPathIndex++ - checkNextPath() - } - } + if (exitCode === 0) { + fileBrowserModal.weAvailable = true + fileBrowserModal.wePath = wePath + } else { + currentPathIndex++ + checkNextPath() + } + } } content: Component { @@ -474,8 +588,8 @@ DankModal { anchors.fill: parent Keys.onPressed: event => { - keyboardController.handleKey(event) - } + keyboardController.handleKey(event) + } onVisibleChanged: { if (visible) { @@ -485,16 +599,17 @@ DankModal { Column { anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS + spacing: 0 Item { width: parent.width - height: 40 + height: 48 Row { spacing: Theme.spacingM anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL DankIcon { name: browserIcon @@ -514,9 +629,37 @@ DankModal { Row { anchors.right: parent.right + anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS + DankActionButton { + circular: false + iconName: showHiddenFiles ? "visibility_off" : "visibility" + iconSize: Theme.iconSize - 4 + iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText + visible: !weMode + onClicked: showHiddenFiles = !showHiddenFiles + } + + DankActionButton { + circular: false + iconName: viewMode === "grid" ? "view_list" : "grid_view" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + visible: !weMode + onClicked: viewMode = viewMode === "grid" ? "list" : "grid" + } + + DankActionButton { + circular: false + iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + visible: !weMode && viewMode === "grid" + onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length + } + DankActionButton { circular: false iconName: "movie" @@ -551,452 +694,298 @@ DankModal { } } - Row { + StyledRect { width: parent.width - spacing: Theme.spacingS - - StyledRect { - width: 32 - height: 32 - radius: Theme.cornerRadius - color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent" - opacity: currentPath !== homeDir ? 1 : 0 - - DankIcon { - anchors.centerIn: parent - name: "arrow_back" - size: Theme.iconSizeSmall - color: Theme.surfaceText - } - - MouseArea { - id: backButtonMouseArea - - anchors.fill: parent - hoverEnabled: currentPath !== homeDir - cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: currentPath !== homeDir - onClicked: navigateUp() - } - } - - StyledText { - text: fileBrowserModal.currentPath.replace("file://", "") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - width: parent.width - 40 - Theme.spacingS - elide: Text.ElideMiddle - anchors.verticalCenter: parent.verticalCenter - maximumLineCount: 1 - wrapMode: Text.NoWrap - } + height: 1 + color: Theme.outline } - DankGridView { - id: fileGrid - + Item { width: parent.width - height: parent.height - 80 - clip: true - cellWidth: weMode ? 255 : 150 - cellHeight: weMode ? 215 : 130 - cacheBuffer: 260 - model: folderModel - currentIndex: selectedIndex - onCurrentIndexChanged: { - if (keyboardNavigationActive && currentIndex >= 0) - positionViewAtIndex(currentIndex, GridView.Contain) - } + height: parent.height - 49 - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } + Row { + anchors.fill: parent + spacing: 0 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AlwaysOff - } + Row { + width: showSidebar ? 201 : 0 + height: parent.height + spacing: 0 + visible: showSidebar - delegate: StyledRect { - id: delegateRoot - - required property bool fileIsDir - required property string filePath - required property string fileName - required property int index - - width: weMode ? 245 : 140 - height: weMode ? 205 : 120 - radius: Theme.cornerRadius - color: { - if (keyboardNavigationActive && delegateRoot.index === selectedIndex) - return Theme.surfacePressed - - return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent" - } - border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline - border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0 - // Update file info when this item gets selected via keyboard or initially - Component.onCompleted: { - if (keyboardNavigationActive && delegateRoot.index === selectedIndex) - setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) - } - - // Watch for selectedIndex changes to update file info during keyboard navigation - Connections { - function onSelectedIndexChanged() { - if (keyboardNavigationActive && selectedIndex === delegateRoot.index) - setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) + FileBrowserSidebar { + height: parent.height + quickAccessLocations: fileBrowserModal.quickAccessLocations + currentPath: fileBrowserModal.currentPath + onLocationSelected: path => navigateTo(path) } - target: fileBrowserModal + StyledRect { + width: 1 + height: parent.height + color: Theme.outline + } } Column { - anchors.centerIn: parent - spacing: Theme.spacingXS + width: parent.width - (showSidebar ? 201 : 0) + height: parent.height + spacing: 0 + + FileBrowserNavigation { + width: parent.width + currentPath: fileBrowserModal.currentPath + homeDir: fileBrowserModal.homeDir + backButtonFocused: fileBrowserModal.backButtonFocused + keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive + showSidebar: fileBrowserModal.showSidebar + pathEditMode: fileBrowserModal.pathEditMode + onNavigateUp: fileBrowserModal.navigateUp() + onNavigateTo: path => fileBrowserModal.navigateTo(path) + onPathInputFocusChanged: hasFocus => { + fileBrowserModal.pathInputHasFocus = hasFocus + if (hasFocus) { + fileBrowserModal.pathEditMode = true + } + } + } + + StyledRect { + width: parent.width + height: 1 + color: Theme.outline + } Item { - width: weMode ? 225 : 80 - height: weMode ? 165 : 60 - anchors.horizontalCenter: parent.horizontalCenter + id: gridContainer + width: parent.width + height: parent.height - 41 + clip: true - CachingImage { + property real gridCellWidth: weMode ? 255 : iconSizes[iconSizeIndex] + 24 + property real gridCellHeight: weMode ? 215 : iconSizes[iconSizeIndex] + 56 + property real availableGridWidth: width - Theme.spacingM * 2 + property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth)) + property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2) + + onGridColumnsChanged: { + fileBrowserModal.actualGridColumns = gridColumns + } + Component.onCompleted: { + fileBrowserModal.actualGridColumns = gridColumns + } + + DankGridView { + id: fileGrid anchors.fill: parent - property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"] - property int weExtIndex: 0 - source: { - if (weMode && delegateRoot.fileIsDir) { - return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex] - } - return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : "" + anchors.leftMargin: gridContainer.gridLeftMargin + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingS + visible: viewMode === "grid" + cellWidth: gridContainer.gridCellWidth + cellHeight: gridContainer.gridCellHeight + cacheBuffer: 260 + model: folderModel + currentIndex: selectedIndex + onCurrentIndexChanged: { + if (keyboardNavigationActive && currentIndex >= 0) + positionViewAtIndex(currentIndex, GridView.Contain) } - onStatusChanged: { - if (weMode && delegateRoot.fileIsDir && status === Image.Error) { - if (weExtIndex < weExtensions.length - 1) { - weExtIndex++ - source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex] - } else { - source = "" + + ScrollBar.vertical: DankScrollbar { + id: gridScrollbar + } + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AlwaysOff + } + + delegate: FileBrowserGridDelegate { + weMode: fileBrowserModal.weMode + iconSizes: fileBrowserModal.iconSizes + iconSizeIndex: fileBrowserModal.iconSizeIndex + selectedIndex: fileBrowserModal.selectedIndex + keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive + onItemClicked: (index, path, name, isDir) => { + selectedIndex = index + setSelectedFileData(path, name, isDir) + if (weMode && isDir) { + var sceneId = path.split("/").pop() + fileSelected("we:" + sceneId) + fileBrowserModal.close() + } else if (isDir) { + navigateTo(path) + } else { + fileSelected(path) + fileBrowserModal.close() + } + } + onItemSelected: (index, path, name, isDir) => { + setSelectedFileData(path, name, isDir) + } + + Connections { + function onKeyboardSelectionRequestedChanged() { + if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) { + fileBrowserModal.keyboardSelectionRequested = false + selectedIndex = index + setSelectedFileData(filePath, fileName, fileIsDir) + if (weMode && fileIsDir) { + var sceneId = filePath.split("/").pop() + fileSelected("we:" + sceneId) + fileBrowserModal.close() + } else if (fileIsDir) { + navigateTo(filePath) + } else { + fileSelected(filePath) + fileBrowserModal.close() + } + } } + + target: fileBrowserModal } } - fillMode: Image.PreserveAspectCrop - visible: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir) - maxCacheSize: weMode ? 225 : 80 } - DankIcon { - anchors.centerIn: parent - name: "description" - size: Theme.iconSizeLarge - color: Theme.primary - visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName) - } + DankListView { + id: fileList + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingS + visible: viewMode === "list" + spacing: 2 + model: folderModel + currentIndex: selectedIndex + onCurrentIndexChanged: { + if (keyboardNavigationActive && currentIndex >= 0) + positionViewAtIndex(currentIndex, ListView.Contain) + } - DankIcon { - anchors.centerIn: parent - name: "folder" - size: Theme.iconSizeLarge - color: Theme.primary - visible: delegateRoot.fileIsDir && !weMode - } - } + ScrollBar.vertical: DankScrollbar { + id: listScrollbar + } - StyledText { - text: delegateRoot.fileName || "" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - width: 120 - elide: Text.ElideMiddle - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter - maximumLineCount: 2 - wrapMode: Text.WordWrap - } - } + delegate: FileBrowserListDelegate { + width: fileList.width + selectedIndex: fileBrowserModal.selectedIndex + keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive + onItemClicked: (index, path, name, isDir) => { + selectedIndex = index + setSelectedFileData(path, name, isDir) + if (isDir) { + navigateTo(path) + } else { + fileSelected(path) + fileBrowserModal.close() + } + } + onItemSelected: (index, path, name, isDir) => { + setSelectedFileData(path, name, isDir) + } - MouseArea { - id: mouseArea + Connections { + function onKeyboardSelectionRequestedChanged() { + if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) { + fileBrowserModal.keyboardSelectionRequested = false + selectedIndex = index + setSelectedFileData(filePath, fileName, fileIsDir) + if (fileIsDir) { + navigateTo(filePath) + } else { + fileSelected(filePath) + fileBrowserModal.close() + } + } + } - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - // Update selected file info and index first - selectedIndex = delegateRoot.index - setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) - if (weMode && delegateRoot.fileIsDir) { - var sceneId = delegateRoot.filePath.split("/").pop() - fileSelected("we:" + sceneId) - fileBrowserModal.close() - } else if (delegateRoot.fileIsDir) { - navigateTo(delegateRoot.filePath) - } else { - fileSelected(delegateRoot.filePath) - fileBrowserModal.close() - } - } - } - - // Handle keyboard selection - Connections { - function onKeyboardSelectionRequestedChanged() { - if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) { - fileBrowserModal.keyboardSelectionRequested = false - selectedIndex = delegateRoot.index - setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) - if (weMode && delegateRoot.fileIsDir) { - var sceneId = delegateRoot.filePath.split("/").pop() - fileSelected("we:" + sceneId) - fileBrowserModal.close() - } else if (delegateRoot.fileIsDir) { - navigateTo(delegateRoot.filePath) - } else { - fileSelected(delegateRoot.filePath) - fileBrowserModal.close() + target: fileBrowserModal + } } } } - - target: fileBrowserModal } } - } - } - Row { - id: saveRow - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Theme.spacingL - height: saveMode ? 40 : 0 - visible: saveMode - spacing: Theme.spacingM - - DankTextField { - id: fileNameInput - - width: parent.width - saveButton.width - Theme.spacingM - height: 40 - text: defaultFileName - placeholderText: I18n.tr("Enter filename...") - ignoreLeftRightKeys: false - focus: saveMode - topPadding: Theme.spacingS - bottomPadding: Theme.spacingS - Component.onCompleted: { - if (saveMode) - Qt.callLater(() => { - forceActiveFocus() - }) + FileBrowserSaveRow { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + saveMode: fileBrowserModal.saveMode + defaultFileName: fileBrowserModal.defaultFileName + currentPath: fileBrowserModal.currentPath + onSaveRequested: filePath => handleSaveFile(filePath) } - onAccepted: { - if (text.trim() !== "") { - // Remove file:// protocol from currentPath if present for proper construction - var basePath = currentPath.replace(/^file:\/\//, '') - var fullPath = basePath + "/" + text.trim() - // Ensure consistent path format - remove any double slashes and normalize - fullPath = fullPath.replace(/\/+/g, '/') - handleSaveFile(fullPath) + + KeyboardHints { + id: keyboardHints + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: fileBrowserModal.showKeyboardHints + } + + FileInfo { + id: fileInfo + + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingL + width: 300 + showFileInfo: fileBrowserModal.showFileInfo + selectedIndex: fileBrowserModal.selectedIndex + sourceFolderModel: folderModel + currentPath: fileBrowserModal.currentPath + currentFileName: fileBrowserModal.selectedFileName + currentFileIsDir: fileBrowserModal.selectedFileIsDir + currentFileExtension: { + if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName) + return "" + + var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.') + return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : "" } } - } - StyledRect { - id: saveButton - - width: 80 - height: 40 - color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant - radius: Theme.cornerRadius - - StyledText { - anchors.centerIn: parent - text: I18n.tr("Save") - color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeMedium - } - - StateLayer { - stateColor: Theme.primary - cornerRadius: Theme.cornerRadius - enabled: fileNameInput.text.trim() !== "" - onClicked: { - if (fileNameInput.text.trim() !== "") { - // Remove file:// protocol from currentPath if present for proper construction - var basePath = currentPath.replace(/^file:\/\//, '') - var fullPath = basePath + "/" + fileNameInput.text.trim() - // Ensure consistent path format - remove any double slashes and normalize - fullPath = fullPath.replace(/\/+/g, '/') - handleSaveFile(fullPath) - } - } + FileBrowserSortMenu { + id: sortMenu + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 120 + anchors.rightMargin: Theme.spacingL + sortBy: fileBrowserModal.sortBy + sortAscending: fileBrowserModal.sortAscending + onSortBySelected: value => { + fileBrowserModal.sortBy = value + } + onSortOrderSelected: ascending => { + fileBrowserModal.sortAscending = ascending + } } } } - KeyboardHints { - id: keyboardHints - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Theme.spacingL - showHints: fileBrowserModal.showKeyboardHints - } - - FileInfo { - id: fileInfo - - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Theme.spacingL - width: 300 - showFileInfo: fileBrowserModal.showFileInfo - selectedIndex: fileBrowserModal.selectedIndex - sourceFolderModel: folderModel - currentPath: fileBrowserModal.currentPath - currentFileName: fileBrowserModal.selectedFileName - currentFileIsDir: fileBrowserModal.selectedFileIsDir - currentFileExtension: { - if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName) - return "" - - var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.') - return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : "" - } - } - - // Overwrite confirmation dialog - Item { - id: overwriteDialog + FileBrowserOverwriteDialog { anchors.fill: parent - visible: showOverwriteConfirmation - - Keys.onEscapePressed: { + showDialog: showOverwriteConfirmation + pendingFilePath: fileBrowserModal.pendingFilePath + onConfirmed: filePath => { + showOverwriteConfirmation = false + fileSelected(filePath) + pendingFilePath = "" + Qt.callLater(() => fileBrowserModal.close()) + } + onCancelled: { showOverwriteConfirmation = false pendingFilePath = "" } - - Keys.onReturnPressed: { - showOverwriteConfirmation = false - fileSelected(pendingFilePath) - pendingFilePath = "" - Qt.callLater(() => fileBrowserModal.close()) - } - - focus: showOverwriteConfirmation - - Rectangle { - anchors.fill: parent - color: Theme.shadowStrong - opacity: 0.8 - - MouseArea { - anchors.fill: parent - onClicked: { - showOverwriteConfirmation = false - pendingFilePath = "" - } - } - } - - StyledRect { - anchors.centerIn: parent - width: 400 - height: 160 - color: Theme.surfaceContainer - radius: Theme.cornerRadius - border.color: Theme.outlineMedium - border.width: 1 - - Column { - anchors.centerIn: parent - width: parent.width - Theme.spacingL * 2 - spacing: Theme.spacingM - - StyledText { - text: I18n.tr("File Already Exists") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("A file with this name already exists. Do you want to overwrite it?") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceTextMedium - width: parent.width - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - spacing: Theme.spacingM - - StyledRect { - width: 80 - height: 36 - radius: Theme.cornerRadius - color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant - border.color: Theme.outline - border.width: 1 - - StyledText { - anchors.centerIn: parent - text: I18n.tr("Cancel") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - MouseArea { - id: cancelArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - showOverwriteConfirmation = false - pendingFilePath = "" - } - } - } - - StyledRect { - width: 90 - height: 36 - radius: Theme.cornerRadius - color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary - - StyledText { - anchors.centerIn: parent - text: I18n.tr("Overwrite") - font.pixelSize: Theme.fontSizeMedium - color: Theme.background - font.weight: Font.Medium - } - - MouseArea { - id: overwriteArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - showOverwriteConfirmation = false - fileSelected(pendingFilePath) - pendingFilePath = "" - Qt.callLater(() => fileBrowserModal.close()) - } - } - } - } - } - } } } } diff --git a/Modals/FileBrowser/FileBrowserNavigation.qml b/Modals/FileBrowser/FileBrowserNavigation.qml new file mode 100644 index 00000000..5c7aa2a4 --- /dev/null +++ b/Modals/FileBrowser/FileBrowserNavigation.qml @@ -0,0 +1,130 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Row { + id: navigation + + property string currentPath: "" + property string homeDir: "" + property bool backButtonFocused: false + property bool keyboardNavigationActive: false + property bool showSidebar: true + property bool pathEditMode: false + property bool pathInputHasFocus: false + + signal navigateUp() + signal navigateTo(string path) + signal pathInputFocusChanged(bool hasFocus) + + height: 40 + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + spacing: Theme.spacingS + + StyledRect { + width: 32 + height: 32 + radius: Theme.cornerRadius + color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent" + opacity: currentPath !== homeDir ? 1 : 0 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "arrow_back" + size: Theme.iconSizeSmall + color: Theme.surfaceText + } + + MouseArea { + id: backButtonMouseArea + + anchors.fill: parent + hoverEnabled: currentPath !== homeDir + cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: currentPath !== homeDir + onClicked: navigation.navigateUp() + } + } + + Item { + width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80)) + height: 32 + anchors.verticalCenter: parent.verticalCenter + + StyledRect { + anchors.fill: parent + radius: Theme.cornerRadius + color: pathEditMode ? Theme.surfaceContainer : "transparent" + border.color: pathEditMode ? Theme.primary : "transparent" + border.width: pathEditMode ? 1 : 0 + visible: !pathEditMode + + StyledText { + id: pathDisplay + text: currentPath.replace("file://", "") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + elide: Text.ElideMiddle + verticalAlignment: Text.AlignVCenter + maximumLineCount: 1 + wrapMode: Text.NoWrap + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + onClicked: { + pathEditMode = true + pathInput.text = currentPath.replace("file://", "") + Qt.callLater(() => pathInput.forceActiveFocus()) + } + } + } + + DankTextField { + id: pathInput + anchors.fill: parent + visible: pathEditMode + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + onAccepted: { + const newPath = text.trim() + if (newPath !== "") { + navigation.navigateTo(newPath) + } + pathEditMode = false + } + Keys.onEscapePressed: { + pathEditMode = false + } + Keys.onDownPressed: { + pathEditMode = false + } + onActiveFocusChanged: { + navigation.pathInputFocusChanged(activeFocus) + if (!activeFocus && pathEditMode) { + pathEditMode = false + } + } + } + } + + Row { + spacing: Theme.spacingXS + visible: !showSidebar + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + circular: false + iconName: "sort" + iconSize: Theme.iconSize - 6 + iconColor: Theme.surfaceText + } + } +} diff --git a/Modals/FileBrowser/FileBrowserOverwriteDialog.qml b/Modals/FileBrowser/FileBrowserOverwriteDialog.qml new file mode 100644 index 00000000..a664be55 --- /dev/null +++ b/Modals/FileBrowser/FileBrowserOverwriteDialog.qml @@ -0,0 +1,127 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: overwriteDialog + + property bool showDialog: false + property string pendingFilePath: "" + + signal confirmed(string filePath) + signal cancelled() + + visible: showDialog + focus: showDialog + + Keys.onEscapePressed: { + cancelled() + } + + Keys.onReturnPressed: { + confirmed(pendingFilePath) + } + + Rectangle { + anchors.fill: parent + color: Theme.shadowStrong + opacity: 0.8 + + MouseArea { + anchors.fill: parent + onClicked: { + cancelled() + } + } + } + + StyledRect { + anchors.centerIn: parent + width: 400 + height: 160 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingL * 2 + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("File Already Exists") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("A file with this name already exists. Do you want to overwrite it?") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + StyledRect { + width: 80 + height: 36 + radius: Theme.cornerRadius + color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant + border.color: Theme.outline + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: I18n.tr("Cancel") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: cancelArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + cancelled() + } + } + } + + StyledRect { + width: 90 + height: 36 + radius: Theme.cornerRadius + color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + StyledText { + anchors.centerIn: parent + text: I18n.tr("Overwrite") + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: overwriteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + confirmed(pendingFilePath) + } + } + } + } + } + } +} diff --git a/Modals/FileBrowser/FileBrowserSaveRow.qml b/Modals/FileBrowser/FileBrowserSaveRow.qml new file mode 100644 index 00000000..6360c7fe --- /dev/null +++ b/Modals/FileBrowser/FileBrowserSaveRow.qml @@ -0,0 +1,74 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Row { + id: saveRow + + property bool saveMode: false + property string defaultFileName: "" + property string currentPath: "" + + signal saveRequested(string filePath) + + height: saveMode ? 40 : 0 + visible: saveMode + spacing: Theme.spacingM + + DankTextField { + id: fileNameInput + + width: parent.width - saveButton.width - Theme.spacingM + height: 40 + text: defaultFileName + placeholderText: I18n.tr("Enter filename...") + ignoreLeftRightKeys: false + focus: saveMode + topPadding: Theme.spacingS + bottomPadding: Theme.spacingS + Component.onCompleted: { + if (saveMode) + Qt.callLater(() => { + forceActiveFocus() + }) + } + onAccepted: { + if (text.trim() !== "") { + var basePath = currentPath.replace(/^file:\/\//, '') + var fullPath = basePath + "/" + text.trim() + fullPath = fullPath.replace(/\/+/g, '/') + saveRequested(fullPath) + } + } + } + + StyledRect { + id: saveButton + + width: 80 + height: 40 + color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: I18n.tr("Save") + color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + } + + StateLayer { + stateColor: Theme.primary + cornerRadius: Theme.cornerRadius + enabled: fileNameInput.text.trim() !== "" + onClicked: { + if (fileNameInput.text.trim() !== "") { + var basePath = currentPath.replace(/^file:\/\//, '') + var fullPath = basePath + "/" + fileNameInput.text.trim() + fullPath = fullPath.replace(/\/+/g, '/') + saveRequested(fullPath) + } + } + } + } +} diff --git a/Modals/FileBrowser/FileBrowserSidebar.qml b/Modals/FileBrowser/FileBrowserSidebar.qml new file mode 100644 index 00000000..c041f5ce --- /dev/null +++ b/Modals/FileBrowser/FileBrowserSidebar.qml @@ -0,0 +1,70 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: sidebar + + property var quickAccessLocations: [] + property string currentPath: "" + signal locationSelected(string path) + + width: 200 + color: Theme.surface + clip: true + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 4 + + StyledText { + text: "Quick Access" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + leftPadding: Theme.spacingS + bottomPadding: Theme.spacingXS + } + + Repeater { + model: quickAccessLocations + + StyledRect { + width: parent?.width ?? 0 + height: 38 + radius: Theme.cornerRadius + color: quickAccessMouseArea.containsMouse ? Theme.surfaceContainerHigh : (currentPath === modelData?.path ? Theme.surfacePressed : "transparent") + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + spacing: Theme.spacingS + + DankIcon { + name: modelData?.icon ?? "" + size: Theme.iconSize - 2 + color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData?.name ?? "" + font.pixelSize: Theme.fontSizeMedium + color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText + font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: quickAccessMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: locationSelected(modelData?.path ?? "") + } + } + } + } +} diff --git a/Modals/FileBrowser/FileBrowserSortMenu.qml b/Modals/FileBrowser/FileBrowserSortMenu.qml new file mode 100644 index 00000000..e1886f9f --- /dev/null +++ b/Modals/FileBrowser/FileBrowserSortMenu.qml @@ -0,0 +1,183 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledRect { + id: sortMenu + + property string sortBy: "name" + property bool sortAscending: true + + signal sortBySelected(string value) + signal sortOrderSelected(bool ascending) + + width: 200 + height: sortColumn.height + Theme.spacingM * 2 + color: Theme.surfaceContainer + radius: Theme.cornerRadius + border.color: Theme.outlineMedium + border.width: 1 + visible: false + z: 100 + + Column { + id: sortColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: "Sort By" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + } + + Repeater { + model: [{ + "name": "Name", + "value": "name" + }, { + "name": "Size", + "value": "size" + }, { + "name": "Modified", + "value": "modified" + }, { + "name": "Type", + "value": "type" + }] + + StyledRect { + width: sortColumn?.width ?? 0 + height: 32 + radius: Theme.cornerRadius + color: sortMouseArea.containsMouse ? Theme.surfaceVariant : (sortBy === modelData?.value ? Theme.surfacePressed : "transparent") + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: sortBy === modelData?.value ? "check" : "" + size: Theme.iconSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + visible: sortBy === modelData?.value + } + + StyledText { + text: modelData?.name ?? "" + font.pixelSize: Theme.fontSizeMedium + color: sortBy === modelData?.value ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: sortMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + sortMenu.sortBySelected(modelData?.value ?? "name") + sortMenu.visible = false + } + } + } + } + + StyledRect { + width: sortColumn.width + height: 1 + color: Theme.outline + } + + StyledText { + text: "Order" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + font.weight: Font.Medium + topPadding: Theme.spacingXS + } + + StyledRect { + width: sortColumn?.width ?? 0 + height: 32 + radius: Theme.cornerRadius + color: ascMouseArea.containsMouse ? Theme.surfaceVariant : (sortAscending ? Theme.surfacePressed : "transparent") + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "arrow_upward" + size: Theme.iconSizeSmall + color: sortAscending ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Ascending" + font.pixelSize: Theme.fontSizeMedium + color: sortAscending ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: ascMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + sortMenu.sortOrderSelected(true) + sortMenu.visible = false + } + } + } + + StyledRect { + width: sortColumn?.width ?? 0 + height: 32 + radius: Theme.cornerRadius + color: descMouseArea.containsMouse ? Theme.surfaceVariant : (!sortAscending ? Theme.surfacePressed : "transparent") + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: "arrow_downward" + size: Theme.iconSizeSmall + color: !sortAscending ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Descending" + font.pixelSize: Theme.fontSizeMedium + color: !sortAscending ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: descMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + sortMenu.sortOrderSelected(false) + sortMenu.visible = false + } + } + } + } +}