summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml920
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml253
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml258
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml97
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml130
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml127
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml74
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml70
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml183
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml66
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml237
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml50
12 files changed, 2465 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml
new file mode 100644
index 0000000..8b96742
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml
new file mode 100644
index 0000000..effa9bd
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml
new file mode 100644
index 0000000..2f452b4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml
new file mode 100644
index 0000000..c7686ca
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml
new file mode 100644
index 0000000..61ce7b6
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml
new file mode 100644
index 0000000..a664be5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml
new file mode 100644
index 0000000..6360c7f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml
new file mode 100644
index 0000000..bc02b3e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml
new file mode 100644
index 0000000..e1886f9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml
new file mode 100644
index 0000000..25b8d5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml
new file mode 100644
index 0000000..c41f039
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml
new file mode 100644
index 0000000..1910810
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/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
+ }
+ }
+}