From 3462bcc46ecf74f576ea4397f16492c16bd75b10 Mon Sep 17 00:00:00 2001 From: Nippy Date: Fri, 8 May 2026 19:50:49 +0200 Subject: raveos update --- .../dms/Modals/FileBrowser/FileBrowserContent.qml | 920 +++++++++++++++++++++ .../Modals/FileBrowser/FileBrowserGridDelegate.qml | 253 ++++++ .../Modals/FileBrowser/FileBrowserListDelegate.qml | 258 ++++++ .../dms/Modals/FileBrowser/FileBrowserModal.qml | 97 +++ .../Modals/FileBrowser/FileBrowserNavigation.qml | 130 +++ .../FileBrowser/FileBrowserOverwriteDialog.qml | 127 +++ .../dms/Modals/FileBrowser/FileBrowserSaveRow.qml | 74 ++ .../dms/Modals/FileBrowser/FileBrowserSidebar.qml | 70 ++ .../dms/Modals/FileBrowser/FileBrowserSortMenu.qml | 183 ++++ .../Modals/FileBrowser/FileBrowserSurfaceModal.qml | 66 ++ .../theme-data/dms/Modals/FileBrowser/FileInfo.qml | 237 ++++++ .../dms/Modals/FileBrowser/KeyboardHints.qml | 50 ++ 12 files changed, 2465 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserContent.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserListDelegate.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserModal.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserNavigation.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserOverwriteDialog.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSaveRow.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSidebar.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSortMenu.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileInfo.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/KeyboardHints.qml (limited to 'raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser') diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserContent.qml new file mode 100644 index 0000000..8b96742 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserContent.qml @@ -0,0 +1,920 @@ +import Qt.labs.folderlistmodel +import QtCore +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +FocusScope { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) + property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation) + property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation) + property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation) + property string currentPath: "" + property var fileExtensions: ["*.*"] + property alias filterExtensions: root.fileExtensions + property string browserTitle: "Select File" + property string browserIcon: "folder_open" + property string browserType: "generic" + property bool showHiddenFiles: false + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property bool backButtonFocused: false + property bool saveMode: false + property string defaultFileName: "" + property int keyboardSelectionIndex: -1 + property bool keyboardSelectionRequested: false + property bool showKeyboardHints: false + property bool showFileInfo: false + property string selectedFilePath: "" + property string selectedFileName: "" + property bool selectedFileIsDir: false + property bool showOverwriteConfirmation: false + property string pendingFilePath: "" + 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 + property bool closeOnEscape: true + property var windowControls: null + + signal fileSelected(string path) + signal closeRequested + + function encodeFileUrl(path) { + if (!path) + return ""; + return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/'); + } + + function initialize() { + loadSettings(); + currentPath = getLastPath(); + _initialized = true; + } + + function reset() { + currentPath = getLastPath(); + selectedIndex = -1; + keyboardNavigationActive = false; + backButtonFocused = false; + } + + 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; + const ext = fileName.toLowerCase().split('.').pop(); + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext); + } + + function getLastPath() { + 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; + } else if (browserType === "profile") { + CacheData.profileLastPath = path; + } + } + + function setSelectedFileData(path, name, isDir) { + selectedFilePath = path; + selectedFileName = name; + selectedFileIsDir = isDir; + } + + function navigateUp() { + const path = currentPath; + if (path === homeDir) + return; + const lastSlash = path.lastIndexOf('/'); + if (lastSlash <= 0) + return; + const newPath = path.substring(0, lastSlash); + if (newPath.length < homeDir.length) { + currentPath = homeDir; + saveLastPath(homeDir); + } else { + currentPath = newPath; + saveLastPath(newPath); + } + } + + function navigateTo(path) { + currentPath = path; + saveLastPath(path); + selectedIndex = -1; + backButtonFocused = false; + } + + function keyboardFileSelection(index) { + if (index < 0) + return; + keyboardSelectionTimer.targetIndex = index; + keyboardSelectionTimer.start(); + } + + function executeKeyboardSelection(index) { + keyboardSelectionIndex = index; + keyboardSelectionRequested = true; + } + + function handleSaveFile(filePath) { + var normalizedPath = filePath; + if (!normalizedPath.startsWith("file://")) { + normalizedPath = encodeFileUrl(filePath); + } + + var exists = false; + var fileName = filePath.split('/').pop(); + + for (var i = 0; i < folderModel.count; i++) { + if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) { + exists = true; + break; + } + } + + if (exists) { + pendingFilePath = normalizedPath; + showOverwriteConfirmation = true; + } else { + fileSelected(normalizedPath); + closeRequested(); + } + } + + onCurrentPathChanged: { + selectedFilePath = ""; + selectedFileName = ""; + selectedFileIsDir = false; + saveSettings(); + } + + onSelectedIndexChanged: { + if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) { + selectedFilePath = ""; + selectedFileName = ""; + selectedFileIsDir = false; + } + } + + 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 quickAccessLocations: [ + { + "name": I18n.tr("Home"), + "path": homeDir, + "icon": "home" + }, + { + "name": I18n.tr("Documents"), + "path": docsDir, + "icon": "description" + }, + { + "name": I18n.tr("Downloads"), + "path": downloadDir, + "icon": "download" + }, + { + "name": I18n.tr("Pictures"), + "path": picsDir, + "icon": "image" + }, + { + "name": I18n.tr("Music"), + "path": musicDir, + "icon": "music_note" + }, + { + "name": I18n.tr("Videos"), + "path": videosDir, + "icon": "movie" + }, + { + "name": I18n.tr("Desktop"), + "path": desktopDir, + "icon": "computer" + } + ] + + FolderListModel { + id: folderModel + + showDirsFirst: true + showDotAndDotDot: false + showHidden: root.showHiddenFiles + caseSensitive: false + nameFilters: fileExtensions + showFiles: true + showDirs: true + folder: encodeFileUrl(currentPath || 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 + } + + QtObject { + id: keyboardController + + property int totalItems: folderModel.count + property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns) + + function handleKey(event) { + if (event.key === Qt.Key_Escape && root.closeOnEscape) { + closeRequested(); + event.accepted = true; + return; + } + if (event.key === Qt.Key_F10) { + showKeyboardHints = !showKeyboardHints; + event.accepted = true; + return; + } + if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) { + showFileInfo = !showFileInfo; + event.accepted = true; + return; + } + if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) { + if (currentPath !== homeDir) { + navigateUp(); + event.accepted = true; + } + 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); + + if (isInitKey) { + keyboardNavigationActive = true; + if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } else { + backButtonFocused = false; + selectedIndex = 0; + } + event.accepted = true; + } + return; + } + switch (event.key) { + case Qt.Key_Tab: + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = 0; + } else if (selectedIndex < totalItems - 1) { + selectedIndex++; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } else { + selectedIndex = 0; + } + event.accepted = true; + break; + case Qt.Key_Backtab: + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = totalItems - 1; + } else if (selectedIndex > 0) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } else { + selectedIndex = totalItems - 1; + } + event.accepted = true; + break; + case Qt.Key_N: + if (event.modifiers & Qt.ControlModifier) { + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = 0; + } else if (selectedIndex < totalItems - 1) { + selectedIndex++; + } + event.accepted = true; + } + break; + case Qt.Key_P: + if (event.modifiers & Qt.ControlModifier) { + if (selectedIndex > 0) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } + event.accepted = true; + } + break; + case Qt.Key_J: + if (event.modifiers & Qt.ControlModifier) { + if (selectedIndex < totalItems - 1) { + selectedIndex++; + } + event.accepted = true; + } + break; + case Qt.Key_K: + if (event.modifiers & Qt.ControlModifier) { + if (selectedIndex > 0) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } + event.accepted = true; + } + break; + case Qt.Key_H: + if (event.modifiers & Qt.ControlModifier) { + if (!backButtonFocused && selectedIndex > 0) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } + event.accepted = true; + } + break; + case Qt.Key_L: + if (event.modifiers & Qt.ControlModifier) { + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = 0; + } else if (selectedIndex < totalItems - 1) { + selectedIndex++; + } + event.accepted = true; + } + break; + case Qt.Key_Left: + if (pathInputHasFocus) + return; + if (backButtonFocused) + return; + if (selectedIndex > 0) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } + event.accepted = true; + break; + case Qt.Key_Right: + if (pathInputHasFocus) + return; + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = 0; + } else if (selectedIndex < totalItems - 1) { + selectedIndex++; + } + event.accepted = true; + break; + case Qt.Key_Up: + if (backButtonFocused) { + backButtonFocused = false; + if (gridColumns === 1) { + selectedIndex = 0; + } else { + var col = selectedIndex % gridColumns; + selectedIndex = Math.min(col, totalItems - 1); + } + } else if (selectedIndex >= gridColumns) { + selectedIndex -= gridColumns; + } else if (selectedIndex > 0 && gridColumns === 1) { + selectedIndex--; + } else if (currentPath !== homeDir) { + backButtonFocused = true; + selectedIndex = -1; + } + event.accepted = true; + break; + case Qt.Key_Down: + if (backButtonFocused) { + backButtonFocused = false; + selectedIndex = 0; + } else if (gridColumns === 1) { + if (selectedIndex < totalItems - 1) { + selectedIndex++; + } + } else { + var newIndex = selectedIndex + gridColumns; + if (newIndex < totalItems) { + selectedIndex = newIndex; + } else { + var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns; + var col = selectedIndex % gridColumns; + var targetIndex = lastRowStart + col; + if (targetIndex < totalItems && targetIndex > selectedIndex) { + selectedIndex = targetIndex; + } + } + } + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + case Qt.Key_Space: + if (backButtonFocused) { + navigateUp(); + } else if (selectedIndex >= 0 && selectedIndex < totalItems) { + root.keyboardFileSelection(selectedIndex); + } + event.accepted = true; + break; + } + } + } + + Timer { + id: keyboardSelectionTimer + + property int targetIndex: -1 + + interval: 1 + onTriggered: { + executeKeyboardSelection(targetIndex); + } + } + + focus: true + + Keys.onPressed: event => { + keyboardController.handleKey(event); + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 48 + + MouseArea { + anchors.fill: parent + onPressed: if (windowControls) + windowControls.tryStartMove() + onDoubleClicked: if (windowControls) + windowControls.tryToggleMaximize() + } + + Row { + spacing: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + + DankIcon { + name: browserIcon + size: Theme.iconSizeLarge + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: browserTitle + font.pixelSize: Theme.fontSizeXLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + 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 + onClicked: showHiddenFiles = !showHiddenFiles + } + + DankActionButton { + circular: false + iconName: viewMode === "grid" ? "view_list" : "grid_view" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + 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: viewMode === "grid" + onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length + } + + DankActionButton { + circular: false + iconName: "info" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.showKeyboardHints = !root.showKeyboardHints + } + + DankActionButton { + visible: windowControls?.supported ?? false + circular: false + iconName: windowControls?.targetWindow?.maximized ? "fullscreen_exit" : "fullscreen" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: if (windowControls) + windowControls.tryToggleMaximize() + } + + DankActionButton { + circular: false + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.closeRequested() + } + } + } + + StyledRect { + width: parent.width + height: 1 + color: Theme.outline + } + + Item { + width: parent.width + height: parent.height - 49 + + Row { + anchors.fill: parent + spacing: 0 + + Row { + width: showSidebar ? 201 : 0 + height: parent.height + spacing: 0 + visible: showSidebar + + FileBrowserSidebar { + height: parent.height + quickAccessLocations: root.quickAccessLocations + currentPath: root.currentPath + onLocationSelected: path => navigateTo(path) + } + + StyledRect { + width: 1 + height: parent.height + color: Theme.outline + } + } + + Column { + width: parent.width - (showSidebar ? 201 : 0) + height: parent.height + spacing: 0 + + FileBrowserNavigation { + width: parent.width + currentPath: root.currentPath + homeDir: root.homeDir + backButtonFocused: root.backButtonFocused + keyboardNavigationActive: root.keyboardNavigationActive + showSidebar: root.showSidebar + pathEditMode: root.pathEditMode + onNavigateUp: root.navigateUp() + onNavigateTo: path => root.navigateTo(path) + onPathInputFocusChanged: hasFocus => { + root.pathInputHasFocus = hasFocus; + if (hasFocus) { + root.pathEditMode = true; + } + } + } + + StyledRect { + width: parent.width + height: 1 + color: Theme.outline + } + + Item { + id: gridContainer + width: parent.width + height: parent.height - 41 + clip: true + + property real gridCellWidth: iconSizes[iconSizeIndex] + 24 + property real gridCellHeight: 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: { + root.actualGridColumns = gridColumns; + } + Component.onCompleted: { + root.actualGridColumns = gridColumns; + } + + DankGridView { + id: fileGrid + anchors.fill: parent + 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); + } + + ScrollBar.vertical: DankScrollbar { + id: gridScrollbar + } + + ScrollBar.horizontal: DankScrollbar { + policy: ScrollBar.AlwaysOff + } + + delegate: FileBrowserGridDelegate { + iconSizes: root.iconSizes + iconSizeIndex: root.iconSizeIndex + selectedIndex: root.selectedIndex + keyboardNavigationActive: root.keyboardNavigationActive + onItemClicked: (index, path, name, isDir) => { + selectedIndex = index; + setSelectedFileData(path, name, isDir); + if (isDir) { + navigateTo(path); + } else { + fileSelected(path); + root.closeRequested(); + } + } + onItemSelected: (index, path, name, isDir) => { + setSelectedFileData(path, name, isDir); + } + + Connections { + function onKeyboardSelectionRequestedChanged() { + if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) { + root.keyboardSelectionRequested = false; + selectedIndex = index; + setSelectedFileData(filePath, fileName, fileIsDir); + if (fileIsDir) { + navigateTo(filePath); + } else { + fileSelected(filePath); + root.closeRequested(); + } + } + } + + target: root + } + } + } + + 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); + } + + ScrollBar.vertical: DankScrollbar { + id: listScrollbar + } + + delegate: FileBrowserListDelegate { + width: fileList.width + selectedIndex: root.selectedIndex + keyboardNavigationActive: root.keyboardNavigationActive + onItemClicked: (index, path, name, isDir) => { + selectedIndex = index; + setSelectedFileData(path, name, isDir); + if (isDir) { + navigateTo(path); + } else { + fileSelected(path); + root.closeRequested(); + } + } + onItemSelected: (index, path, name, isDir) => { + setSelectedFileData(path, name, isDir); + } + + Connections { + function onKeyboardSelectionRequestedChanged() { + if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) { + root.keyboardSelectionRequested = false; + selectedIndex = index; + setSelectedFileData(filePath, fileName, fileIsDir); + if (fileIsDir) { + navigateTo(filePath); + } else { + fileSelected(filePath); + root.closeRequested(); + } + } + } + + target: root + } + } + } + } + } + } + + FileBrowserSaveRow { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + saveMode: root.saveMode + defaultFileName: root.defaultFileName + currentPath: root.currentPath + onSaveRequested: filePath => handleSaveFile(filePath) + } + + KeyboardHints { + id: keyboardHints + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + showHints: root.showKeyboardHints + } + + FileInfo { + id: fileInfo + + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingL + width: 300 + showFileInfo: root.showFileInfo + selectedIndex: root.selectedIndex + sourceFolderModel: folderModel + currentPath: root.currentPath + currentFileName: root.selectedFileName + currentFileIsDir: root.selectedFileIsDir + currentFileExtension: { + if (root.selectedFileIsDir || !root.selectedFileName) + return ""; + + var lastDot = root.selectedFileName.lastIndexOf('.'); + return lastDot > 0 ? root.selectedFileName.substring(lastDot + 1).toLowerCase() : ""; + } + } + + FileBrowserSortMenu { + id: sortMenu + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 120 + anchors.rightMargin: Theme.spacingL + sortBy: root.sortBy + sortAscending: root.sortAscending + onSortBySelected: value => { + root.sortBy = value; + } + onSortOrderSelected: ascending => { + root.sortAscending = ascending; + } + } + } + + FileBrowserOverwriteDialog { + anchors.fill: parent + showDialog: showOverwriteConfirmation + pendingFilePath: root.pendingFilePath + onConfirmed: filePath => { + showOverwriteConfirmation = false; + fileSelected(filePath); + pendingFilePath = ""; + Qt.callLater(() => root.closeRequested()); + } + onCancelled: { + showOverwriteConfirmation = false; + pendingFilePath = ""; + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml new file mode 100644 index 0000000..effa9bd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml @@ -0,0 +1,253 @@ +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 getFileExtension(fileName) { + const parts = fileName.split('.'); + if (parts.length > 1) { + return parts[parts.length - 1].toLowerCase(); + } + return ""; + } + + function determineFileType(fileName) { + const ext = getFileExtension(fileName); + + const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"]; + if (imageExts.includes(ext)) { + return "image"; + } + + const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]; + if (videoExts.includes(ext)) { + return "video"; + } + + const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]; + if (audioExts.includes(ext)) { + return "audio"; + } + + const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]; + if (codeExts.includes(ext)) { + return "code"; + } + + const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]; + if (docExts.includes(ext)) { + return "document"; + } + + const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]; + if (archiveExts.includes(ext)) { + return "archive"; + } + + if (!ext || fileName.indexOf('.') === -1) { + return "binary"; + } + + return "file"; + } + + function isImageFile(fileName) { + if (!fileName) { + return false; + } + return determineFileType(fileName) === "image"; + } + + function isVideoFile(fileName) { + if (!fileName) { + return false; + } + return determineFileType(fileName) === "video"; + } + + property bool isImage: isImageFile(delegateRoot.fileName) + property bool isVideo: isVideoFile(delegateRoot.fileName) + + property string _xdgCacheHome: Paths.strip(Paths.xdgCache) + property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large" + property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256 + property string videoThumbnailPath: { + if (!delegateRoot.fileIsDir && isVideo) { + const hash = Qt.md5("file://" + delegateRoot.filePath); + return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png"; + } + return ""; + } + + property string _videoThumb: "" + + onVideoThumbnailPathChanged: { + _videoThumb = ""; + if (!videoThumbnailPath) + return; + const thumbPath = videoThumbnailPath; + const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize; + const size = _thumbnailPx; + const fp = delegateRoot.filePath; + Paths.mkdir(thumbDir); + Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) { + if (exitCode === 0) { + _videoThumb = thumbPath; + } else { + Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) { + if (exitCode === 0) + _videoThumb = thumbPath; + }); + } + }); + } + + function getIconForFile(fileName) { + const lowerName = fileName.toLowerCase(); + if (lowerName.startsWith("dockerfile")) { + return "docker"; + } + const ext = fileName.split('.').pop(); + return ext || ""; + } + + 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.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "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 + + Image { + id: gridPreviewImage + anchors.fill: parent + anchors.margins: 2 + property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"] + property int weExtIndex: 0 + property string imagePath: { + if (weMode && delegateRoot.fileIsDir) + return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]; + if (!delegateRoot.fileIsDir && isImage) + return delegateRoot.filePath; + if (_videoThumb) + return _videoThumb; + return ""; + } + source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : "" + onStatusChanged: { + if (weMode && delegateRoot.fileIsDir && status === Image.Error) { + if (weExtIndex < weExtensions.length - 1) { + weExtIndex++; + } else { + imagePath = ""; + } + } + } + fillMode: Image.PreserveAspectCrop + sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex] + sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex] + asynchronous: true + visible: false + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: gridPreviewImage + maskEnabled: true + maskSource: gridImageMask + visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (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 + } + } + + DankNFIcon { + anchors.centerIn: parent + name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName) + size: iconSizes[iconSizeIndex] * 0.45 + color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText + visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserListDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserListDelegate.qml new file mode 100644 index 0000000..2f452b4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserListDelegate.qml @@ -0,0 +1,258 @@ +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 getFileExtension(fileName) { + const parts = fileName.split('.'); + if (parts.length > 1) { + return parts[parts.length - 1].toLowerCase(); + } + return ""; + } + + function determineFileType(fileName) { + const ext = getFileExtension(fileName); + + const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"]; + if (imageExts.includes(ext)) { + return "image"; + } + + const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]; + if (videoExts.includes(ext)) { + return "video"; + } + + const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]; + if (audioExts.includes(ext)) { + return "audio"; + } + + const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]; + if (codeExts.includes(ext)) { + return "code"; + } + + const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]; + if (docExts.includes(ext)) { + return "document"; + } + + const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]; + if (archiveExts.includes(ext)) { + return "archive"; + } + + if (!ext || fileName.indexOf('.') === -1) { + return "binary"; + } + + return "file"; + } + + function isImageFile(fileName) { + if (!fileName) { + return false; + } + return determineFileType(fileName) === "image"; + } + + function isVideoFile(fileName) { + if (!fileName) { + return false; + } + return determineFileType(fileName) === "video"; + } + + property bool isImage: isImageFile(listDelegateRoot.fileName) + property bool isVideo: isVideoFile(listDelegateRoot.fileName) + + property string _xdgCacheHome: Paths.strip(Paths.xdgCache) + property string videoThumbnailPath: { + if (!listDelegateRoot.fileIsDir && isVideo) { + const hash = Qt.md5("file://" + listDelegateRoot.filePath); + return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png"; + } + return ""; + } + + property string _videoThumb: "" + + onVideoThumbnailPathChanged: { + _videoThumb = ""; + if (!videoThumbnailPath) + return; + const thumbPath = videoThumbnailPath; + const fp = listDelegateRoot.filePath; + Paths.mkdir(_xdgCacheHome + "/thumbnails/normal"); + Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) { + if (exitCode === 0) { + _videoThumb = thumbPath; + } else { + Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) { + if (exitCode === 0) + _videoThumb = thumbPath; + }); + } + }); + } + + function getIconForFile(fileName) { + const lowerName = fileName.toLowerCase(); + if (lowerName.startsWith("dockerfile")) { + return "docker"; + } + const ext = fileName.split('.').pop(); + return ext || ""; + } + + 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.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "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 + + Image { + id: listPreviewImage + anchors.fill: parent + property string imagePath: { + if (!listDelegateRoot.fileIsDir && isImage) + return listDelegateRoot.filePath; + if (_videoThumb) + return _videoThumb; + return ""; + } + source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : "" + fillMode: Image.PreserveAspectCrop + sourceSize.width: 32 + sourceSize.height: 32 + asynchronous: true + visible: false + } + + MultiEffect { + anchors.fill: parent + source: listPreviewImage + maskEnabled: true + maskSource: listImageMask + visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo) + 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 + } + } + + DankNFIcon { + anchors.centerIn: parent + name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName) + size: Theme.iconSize - 2 + color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText + visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready)) + } + } + + 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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserModal.qml new file mode 100644 index 0000000..c7686ca --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserModal.qml @@ -0,0 +1,97 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +FloatingWindow { + id: fileBrowserModal + + property bool disablePopupTransparency: true + property string browserTitle: "Select File" + property string browserIcon: "folder_open" + property string browserType: "generic" + property var fileExtensions: ["*.*"] + property alias filterExtensions: fileBrowserModal.fileExtensions + property bool showHiddenFiles: false + property bool saveMode: false + property string defaultFileName: "" + property var parentModal: null + property bool shouldHaveFocus: visible + property bool allowFocusOverride: false + property bool shouldBeVisible: visible + property bool allowStacking: true + + signal fileSelected(string path) + signal dialogClosed + + function open() { + visible = true; + } + + function close() { + visible = false; + } + + objectName: "fileBrowserModal" + title: "Files - " + browserTitle + minimumSize: Qt.size(500, 400) + implicitWidth: 800 + implicitHeight: 600 + color: Theme.surfaceContainer + visible: false + + onVisibleChanged: { + if (visible) { + if (parentModal && "shouldHaveFocus" in parentModal) { + parentModal.shouldHaveFocus = false; + parentModal.allowFocusOverride = true; + } + Qt.callLater(() => { + if (content) { + content.reset(); + content.forceActiveFocus(); + } + }); + } else { + if (parentModal && "allowFocusOverride" in parentModal) { + parentModal.allowFocusOverride = false; + parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible); + } + dialogClosed(); + } + } + + Loader { + id: contentLoader + anchors.fill: parent + active: fileBrowserModal.visible + sourceComponent: FileBrowserContent { + id: content + anchors.fill: parent + focus: true + closeOnEscape: false + windowControls: fileBrowserModal.windowControlsRef + + browserTitle: fileBrowserModal.browserTitle + browserIcon: fileBrowserModal.browserIcon + browserType: fileBrowserModal.browserType + fileExtensions: fileBrowserModal.fileExtensions + showHiddenFiles: fileBrowserModal.showHiddenFiles + saveMode: fileBrowserModal.saveMode + defaultFileName: fileBrowserModal.defaultFileName + + Component.onCompleted: initialize() + + onFileSelected: path => fileBrowserModal.fileSelected(path) + onCloseRequested: fileBrowserModal.close() + } + } + + property alias content: contentLoader.item + property alias windowControlsRef: windowControls + + FloatingWindowControls { + id: windowControls + targetWindow: fileBrowserModal + } +} diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserNavigation.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserNavigation.qml new file mode 100644 index 0000000..61ce7b6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : "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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserOverwriteDialog.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserOverwriteDialog.qml new file mode 100644 index 0000000..a664be5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSaveRow.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSaveRow.qml new file mode 100644 index 0000000..6360c7f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSidebar.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSidebar.qml new file mode 100644 index 0000000..bc02b3e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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: I18n.tr("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.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : (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/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSortMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSortMenu.qml new file mode 100644 index 0000000..e1886f9 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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 + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml new file mode 100644 index 0000000..25b8d5a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml @@ -0,0 +1,66 @@ +import QtQuick +import Quickshell.Wayland +import qs.Common +import qs.Modals.Common + +DankModal { + id: fileBrowserSurfaceModal + + property string browserTitle: "Select File" + property string browserIcon: "folder_open" + property string browserType: "generic" + property var fileExtensions: ["*.*"] + property alias filterExtensions: fileBrowserSurfaceModal.fileExtensions + property bool showHiddenFiles: false + property bool saveMode: false + property string defaultFileName: "" + property var parentPopout: null + + signal fileSelected(string path) + + layerNamespace: "dms:filebrowser" + modalWidth: 800 + modalHeight: 600 + backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + closeOnEscapeKey: true + closeOnBackgroundClick: true + allowStacking: true + keepPopoutsOpen: true + + onBackgroundClicked: close() + + onOpened: { + if (parentPopout) { + parentPopout.customKeyboardFocus = WlrKeyboardFocus.None; + } + Qt.callLater(() => { + if (contentLoader.item) { + contentLoader.item.reset(); + contentLoader.item.forceActiveFocus(); + } + }); + } + + onDialogClosed: { + if (parentPopout) { + parentPopout.customKeyboardFocus = null; + } + } + + content: FileBrowserContent { + focus: true + + browserTitle: fileBrowserSurfaceModal.browserTitle + browserIcon: fileBrowserSurfaceModal.browserIcon + browserType: fileBrowserSurfaceModal.browserType + fileExtensions: fileBrowserSurfaceModal.fileExtensions + showHiddenFiles: fileBrowserSurfaceModal.showHiddenFiles + saveMode: fileBrowserSurfaceModal.saveMode + defaultFileName: fileBrowserSurfaceModal.defaultFileName + + Component.onCompleted: initialize() + + onFileSelected: path => fileBrowserSurfaceModal.fileSelected(path) + onCloseRequested: fileBrowserSurfaceModal.close() + } +} diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileInfo.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileInfo.qml new file mode 100644 index 0000000..c41f039 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileInfo.qml @@ -0,0 +1,237 @@ +import QtQuick +import QtCore +import Quickshell.Io +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showFileInfo: false + property int selectedIndex: -1 + property var sourceFolderModel: null + property string currentPath: "" + + height: 200 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.secondary + border.width: 2 + opacity: showFileInfo ? 1 : 0 + z: 100 + + onShowFileInfoChanged: { + if (showFileInfo && currentFileName && currentPath) { + const fullPath = currentPath + "/" + currentFileName + fileStatProcess.selectedFilePath = fullPath + fileStatProcess.running = true + } + } + + Process { + id: fileStatProcess + command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath] + property string selectedFilePath: "" + property var fileStats: null + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + const parts = text.trim().split('|') + if (parts.length >= 4) { + fileStatProcess.fileStats = { + "modifiedTime": parts[0], + "permissions": parts[1], + "size": parseInt(parts[2]) || 0, + "fullPath": parts[3] + } + } + } + } + } + + onExited: function (exitCode) {} + } + + property string currentFileName: "" + property bool currentFileIsDir: false + property string currentFileExtension: "" + + onCurrentFileNameChanged: { + if (showFileInfo && currentFileName && currentPath) { + const fullPath = currentPath + "/" + currentFileName + if (fullPath !== fileStatProcess.selectedFilePath) { + fileStatProcess.selectedFilePath = fullPath + fileStatProcess.running = true + } + } + } + + function updateFileInfo(filePath, fileName, isDirectory) { + if (filePath && filePath !== fileStatProcess.selectedFilePath) { + fileStatProcess.selectedFilePath = filePath + currentFileName = fileName || "" + currentFileIsDir = isDirectory || false + + let ext = "" + if (!isDirectory && fileName) { + const lastDot = fileName.lastIndexOf('.') + if (lastDot > 0) { + ext = fileName.substring(lastDot + 1).toLowerCase() + } + } + currentFileExtension = ext + + if (showFileInfo) { + fileStatProcess.running = true + } + } + } + + readonly property var currentFileDisplayData: { + if (selectedIndex < 0 || !sourceFolderModel) { + return { + "exists": false, + "name": "No selection", + "type": "", + "size": "", + "modified": "", + "permissions": "", + "extension": "", + "position": "N/A" + } + } + + const hasValidFile = currentFileName !== "" + return { + "exists": hasValidFile, + "name": hasValidFile ? currentFileName : "Loading...", + "type": currentFileIsDir ? "Directory" : "File", + "size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...", + "modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...", + "permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...", + "extension": currentFileExtension, + "position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A" + } + } + + Column { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + spacing: Theme.spacingXS + + Row { + width: parent.width + spacing: Theme.spacingS + + DankIcon { + name: "info" + size: Theme.iconSize + color: Theme.secondary + } + + StyledText { + text: I18n.tr("File Information") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + + StyledText { + text: currentFileDisplayData.name + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + elide: Text.ElideMiddle + wrapMode: Text.NoWrap + font.weight: Font.Medium + } + + StyledText { + text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + } + + StyledText { + text: currentFileDisplayData.size + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + visible: currentFileDisplayData.exists && !currentFileIsDir + } + + StyledText { + text: currentFileDisplayData.modified + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + elide: Text.ElideRight + visible: currentFileDisplayData.exists + } + + StyledText { + text: currentFileDisplayData.permissions + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: currentFileDisplayData.exists + } + + StyledText { + text: currentFileDisplayData.position + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + width: parent.width + } + } + } + + StyledText { + text: I18n.tr("F1/I: Toggle • F10: Help") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingM + horizontalAlignment: Text.AlignHCenter + } + + function formatFileSize(bytes) { + if (bytes === 0 || !bytes) { + return "0 B" + } + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] + } + + function formatDateTime(dateTimeString) { + if (!dateTimeString) { + return "Unknown" + } + const parts = dateTimeString.split(' ') + if (parts.length >= 2) { + return parts[0] + " " + parts[1].split('.')[0] + } + return dateTimeString + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/KeyboardHints.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/KeyboardHints.qml new file mode 100644 index 0000000..1910810 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/KeyboardHints.qml @@ -0,0 +1,50 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool showHints: false + + height: 80 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: showHints ? 1 : 0 + z: 100 + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingS + spacing: 2 + + StyledText { + text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StyledText { + text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} -- cgit v1.3