1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 16:32:50 -05:00

filebrowser: improved file browser

This commit is contained in:
bbedward
2025-10-25 23:30:33 -04:00
parent f7e8de2556
commit 968606d781
9 changed files with 1543 additions and 453 deletions

View File

@@ -22,6 +22,57 @@ Singleton {
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property var fileBrowserSettings: ({
"wallpaper": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"profile": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_save": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_load": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"generic": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"default": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
})
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadCache() loadCache()
@@ -43,6 +94,37 @@ Singleton {
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "" wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "" profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
if (cache.fileBrowserSettings !== undefined) {
fileBrowserSettings = cache.fileBrowserSettings
} else if (cache.fileBrowserViewMode !== undefined) {
fileBrowserSettings = {
"wallpaper": {
"lastPath": cache.wallpaperLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"profile": {
"lastPath": cache.profileLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"file": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
}
}
if (cache.configVersion === undefined) { if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache) migrateFromUndefinedToV1(cache)
cleanupUnusedKeys() cleanupUnusedKeys()
@@ -62,6 +144,7 @@ Singleton {
cacheFile.setText(JSON.stringify({ cacheFile.setText(JSON.stringify({
"wallpaperLastPath": wallpaperLastPath, "wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath, "profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion "configVersion": cacheConfigVersion
}, null, 2)) }, null, 2))
} }
@@ -74,6 +157,7 @@ Singleton {
const validKeys = [ const validKeys = [
"wallpaperLastPath", "wallpaperLastPath",
"profileLastPath", "profileLastPath",
"fileBrowserSettings",
"configVersion" "configVersion"
] ]

View File

@@ -0,0 +1,214 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Widgets
StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property int index
property bool weMode: false
property var iconSizes: [80, 120, 160, 200]
property int iconSizeIndex: 1
property int selectedIndex: -1
property bool keyboardNavigationActive: false
signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir)
function isImageFile(fileName) {
if (!fileName) {
return false
}
const ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getFileIcon(fileName, isDir) {
if (isDir) {
return "folder"
}
if (!fileName) {
return "description"
}
const ext = fileName.toLowerCase().split('.').pop()
const iconMap = {
"mp3": 'music_note',
"wav": 'music_note',
"flac": 'music_note',
"ogg": 'music_note',
"aac": 'music_note',
"mp4": 'movie',
"mkv": 'movie',
"avi": 'movie',
"mov": 'movie',
"webm": 'movie',
"flv": 'movie',
"wmv": 'movie',
"jpg": 'image',
"jpeg": 'image',
"png": 'image',
"gif": 'image',
"bmp": 'image',
"webp": 'image',
"svg": 'image',
"pdf": 'picture_as_pdf',
"zip": 'folder_zip',
"rar": 'folder_zip',
"7z": 'folder_zip',
"tar": 'folder_zip',
"gz": 'folder_zip',
"bz2": 'folder_zip',
"xz": 'folder_zip',
"txt": 'description',
"md": 'description',
"doc": 'description',
"docx": 'description',
"odt": 'description',
"rtf": 'description',
"sh": 'terminal',
"py": 'code',
"js": 'code',
"ts": 'code',
"cpp": 'code',
"c": 'code',
"h": 'code',
"java": 'code',
"go": 'code',
"rs": 'code',
"php": 'code',
"rb": 'code',
"qml": 'code',
"html": 'code',
"css": 'code',
"json": 'data_object',
"xml": 'data_object',
"yaml": 'data_object',
"yml": 'data_object',
"toml": 'data_object'
}
return iconMap[ext] || 'description'
}
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
return Theme.surfacePressed
return mouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8)
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
id: gridPreviewImage
anchors.fill: parent
anchors.margins: 2
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
property int weExtIndex: 0
source: {
if (weMode && delegateRoot.fileIsDir) {
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
}
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
}
onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
} else {
source = ""
}
}
}
fillMode: Image.PreserveAspectCrop
maxCacheSize: weMode ? 225 : iconSizes[iconSizeIndex]
visible: false
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: gridPreviewImage
maskEnabled: true
maskSource: gridImageMask
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir))
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: gridImageMask
anchors.fill: parent
anchors.margins: 2
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: getFileIcon(delegateRoot.fileName, delegateRoot.fileIsDir)
size: iconSizes[iconSizeIndex] * 0.45
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode)
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: delegateRoot.width - Theme.spacingM
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.Wrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
}
}

View File

@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Widgets
StyledRect {
id: listDelegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property int index
required property var fileModified
required property int fileSize
property int selectedIndex: -1
property bool keyboardNavigationActive: false
signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir)
function isImageFile(fileName) {
if (!fileName) {
return false
}
const ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getFileIcon(fileName, isDir) {
if (isDir) {
return "folder"
}
if (!fileName) {
return "description"
}
const ext = fileName.toLowerCase().split('.').pop()
const iconMap = {
"mp3": 'music_note',
"wav": 'music_note',
"flac": 'music_note',
"ogg": 'music_note',
"aac": 'music_note',
"mp4": 'movie',
"mkv": 'movie',
"avi": 'movie',
"mov": 'movie',
"webm": 'movie',
"flv": 'movie',
"wmv": 'movie',
"jpg": 'image',
"jpeg": 'image',
"png": 'image',
"gif": 'image',
"bmp": 'image',
"webp": 'image',
"svg": 'image',
"pdf": 'picture_as_pdf',
"zip": 'folder_zip',
"rar": 'folder_zip',
"7z": 'folder_zip',
"tar": 'folder_zip',
"gz": 'folder_zip',
"bz2": 'folder_zip',
"xz": 'folder_zip',
"txt": 'description',
"md": 'description',
"doc": 'description',
"docx": 'description',
"odt": 'description',
"rtf": 'description',
"sh": 'terminal',
"py": 'code',
"js": 'code',
"ts": 'code',
"cpp": 'code',
"c": 'code',
"h": 'code',
"java": 'code',
"go": 'code',
"rs": 'code',
"php": 'code',
"rb": 'code',
"qml": 'code',
"html": 'code',
"css": 'code',
"json": 'data_object',
"xml": 'data_object',
"yaml": 'data_object',
"yml": 'data_object',
"toml": 'data_object'
}
return iconMap[ext] || 'description'
}
function formatFileSize(size) {
if (size < 1024)
return size + " B"
if (size < 1024 * 1024)
return (size / 1024).toFixed(1) + " KB"
if (size < 1024 * 1024 * 1024)
return (size / (1024 * 1024)).toFixed(1) + " MB"
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB"
}
height: 44
radius: Theme.cornerRadius
color: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
return Theme.surfacePressed
return listMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
}
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
Item {
width: 28
height: 28
anchors.verticalCenter: parent.verticalCenter
CachingImage {
id: listPreviewImage
anchors.fill: parent
source: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? ("file://" + listDelegateRoot.filePath) : ""
fillMode: Image.PreserveAspectCrop
maxCacheSize: 32
visible: false
}
MultiEffect {
anchors.fill: parent
source: listPreviewImage
maskEnabled: true
maskSource: listImageMask
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: listImageMask
anchors.fill: parent
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: getFileIcon(listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
size: Theme.iconSize - 2
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName)
}
}
StyledText {
text: listDelegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width - 280
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
clip: true
}
StyledText {
text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: 70
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: 140
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: listMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir)
}
}
}

View File

@@ -5,6 +5,7 @@ import QtQuick.Controls
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Widgets import qs.Widgets
DankModal { DankModal {
@@ -16,13 +17,13 @@ DankModal {
property alias filterExtensions: fileBrowserModal.fileExtensions property alias filterExtensions: fileBrowserModal.fileExtensions
property string browserTitle: "Select File" property string browserTitle: "Select File"
property string browserIcon: "folder_open" property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory property string browserType: "generic"
property bool showHiddenFiles: false property bool showHiddenFiles: false
property int selectedIndex: -1 property int selectedIndex: -1
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property bool backButtonFocused: false property bool backButtonFocused: false
property bool saveMode: false // Enable save functionality property bool saveMode: false
property string defaultFileName: "" // Default filename for save mode property string defaultFileName: ""
property int keyboardSelectionIndex: -1 property int keyboardSelectionIndex: -1
property bool keyboardSelectionRequested: false property bool keyboardSelectionRequested: false
property bool showKeyboardHints: false property bool showKeyboardHints: false
@@ -36,9 +37,67 @@ DankModal {
property string wePath: "" property string wePath: ""
property bool weMode: false property bool weMode: false
property var parentModal: null property var parentModal: null
property bool showSidebar: true
property string viewMode: "grid"
property string sortBy: "name"
property bool sortAscending: true
property int iconSizeIndex: 1
property var iconSizes: [80, 120, 160, 200]
property bool pathEditMode: false
property bool pathInputHasFocus: false
property int actualGridColumns: 5
property bool _initialized: false
signal fileSelected(string path) signal fileSelected(string path)
function loadSettings() {
const type = browserType || "default"
const settings = CacheData.fileBrowserSettings[type]
const isImageBrowser = ["wallpaper", "profile"].includes(browserType)
if (settings) {
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list")
sortBy = settings.sortBy || "name"
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true
} else {
viewMode = isImageBrowser ? "grid" : "list"
}
}
function saveSettings() {
if (!_initialized)
return
const type = browserType || "default"
let settings = CacheData.fileBrowserSettings
if (!settings[type]) {
settings[type] = {}
}
settings[type].viewMode = viewMode
settings[type].sortBy = sortBy
settings[type].sortAscending = sortAscending
settings[type].iconSizeIndex = iconSizeIndex
settings[type].showSidebar = showSidebar
settings[type].lastPath = currentPath
CacheData.fileBrowserSettings = settings
if (browserType === "wallpaper") {
CacheData.wallpaperLastPath = currentPath
} else if (browserType === "profile") {
CacheData.profileLastPath = currentPath
}
CacheData.saveCache()
}
onViewModeChanged: saveSettings()
onSortByChanged: saveSettings()
onSortAscendingChanged: saveSettings()
onIconSizeIndexChanged: saveSettings()
onShowSidebarChanged: saveSettings()
function isImageFile(fileName) { function isImageFile(fileName) {
if (!fileName) { if (!fileName) {
return false return false
@@ -48,17 +107,26 @@ DankModal {
} }
function getLastPath() { function getLastPath() {
const lastPath = browserType === "wallpaper" ? CacheData.wallpaperLastPath : browserType === "profile" ? CacheData.profileLastPath : "" const type = browserType || "default"
const settings = CacheData.fileBrowserSettings[type]
const lastPath = settings?.lastPath || ""
return (lastPath && lastPath !== "") ? lastPath : homeDir return (lastPath && lastPath !== "") ? lastPath : homeDir
} }
function saveLastPath(path) { 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") { if (browserType === "wallpaper") {
CacheData.wallpaperLastPath = path CacheData.wallpaperLastPath = path
CacheData.saveCache()
} else if (browserType === "profile") { } else if (browserType === "profile") {
CacheData.profileLastPath = path CacheData.profileLastPath = path
CacheData.saveCache()
} }
} }
@@ -106,13 +174,11 @@ DankModal {
} }
function handleSaveFile(filePath) { function handleSaveFile(filePath) {
// Ensure the filePath has the correct file:// protocol format
var normalizedPath = filePath var normalizedPath = filePath
if (!normalizedPath.startsWith("file://")) { if (!normalizedPath.startsWith("file://")) {
normalizedPath = "file://" + filePath normalizedPath = "file://" + filePath
} }
// Check if file exists by looking through the folder model
var exists = false var exists = false
var fileName = filePath.split('/').pop() var fileName = filePath.split('/').pop()
@@ -137,15 +203,15 @@ DankModal {
closeOnEscapeKey: false closeOnEscapeKey: false
shouldHaveFocus: shouldBeVisible shouldHaveFocus: shouldBeVisible
Component.onCompleted: { Component.onCompleted: {
loadSettings()
currentPath = getLastPath() currentPath = getLastPath()
_initialized = true
} }
property var steamPaths: [ property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"
]
property int currentPathIndex: 0 property int currentPathIndex: 0
function discoverWallpaperEngine() { function discoverWallpaperEngine() {
@@ -203,6 +269,7 @@ DankModal {
selectedFilePath = "" selectedFilePath = ""
selectedFileName = "" selectedFileName = ""
selectedFileIsDir = false selectedFileIsDir = false
saveSettings()
} }
onSelectedIndexChanged: { onSelectedIndexChanged: {
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) { if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
@@ -222,13 +289,58 @@ DankModal {
showFiles: true showFiles: true
showDirs: true showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir folder: currentPath ? "file://" + currentPath : "file://" + homeDir
sortField: {
switch (sortBy) {
case "name":
return FolderListModel.Name
case "size":
return FolderListModel.Size
case "modified":
return FolderListModel.Time
case "type":
return FolderListModel.Type
default:
return FolderListModel.Name
} }
}
sortReversed: !sortAscending
}
property var quickAccessLocations: [{
"name": "Home",
"path": homeDir,
"icon": "home"
}, {
"name": "Documents",
"path": homeDir + "/Documents",
"icon": "description"
}, {
"name": "Downloads",
"path": homeDir + "/Downloads",
"icon": "download"
}, {
"name": "Pictures",
"path": homeDir + "/Pictures",
"icon": "image"
}, {
"name": "Music",
"path": homeDir + "/Music",
"icon": "music_note"
}, {
"name": "Videos",
"path": homeDir + "/Videos",
"icon": "movie"
}, {
"name": "Desktop",
"path": homeDir + "/Desktop",
"icon": "computer"
}]
QtObject { QtObject {
id: keyboardController id: keyboardController
property int totalItems: folderModel.count property int totalItems: folderModel.count
property int gridColumns: 5 property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) { function handleKey(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
@@ -236,19 +348,16 @@ DankModal {
event.accepted = true event.accepted = true
return return
} }
// F10 toggles keyboard hints
if (event.key === Qt.Key_F10) { if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints showKeyboardHints = !showKeyboardHints
event.accepted = true event.accepted = true
return return
} }
// F1 or I key for file information
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) { if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
showFileInfo = !showFileInfo showFileInfo = !showFileInfo
event.accepted = true event.accepted = true
return return
} }
// Alt+Left or Backspace to go back
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) { if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
if (currentPath !== homeDir) { if (currentPath !== homeDir) {
navigateUp() navigateUp()
@@ -257,10 +366,8 @@ DankModal {
return return
} }
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key
(event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || === 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)
(event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) ||
(event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
if (isInitKey) { if (isInitKey) {
keyboardNavigationActive = true keyboardNavigationActive = true
@@ -368,6 +475,8 @@ DankModal {
} }
break break
case Qt.Key_Left: case Qt.Key_Left:
if (pathInputHasFocus)
return
if (backButtonFocused) if (backButtonFocused)
return return
@@ -380,6 +489,9 @@ DankModal {
event.accepted = true event.accepted = true
break break
case Qt.Key_Right: case Qt.Key_Right:
if (pathInputHasFocus)
return
if (backButtonFocused) { if (backButtonFocused) {
backButtonFocused = false backButtonFocused = false
selectedIndex = 0 selectedIndex = 0
@@ -391,14 +503,17 @@ DankModal {
case Qt.Key_Up: case Qt.Key_Up:
if (backButtonFocused) { if (backButtonFocused) {
backButtonFocused = false backButtonFocused = false
// Go to first row, appropriate column if (gridColumns === 1) {
selectedIndex = 0
} else {
var col = selectedIndex % gridColumns var col = selectedIndex % gridColumns
selectedIndex = Math.min(col, totalItems - 1) selectedIndex = Math.min(col, totalItems - 1)
}
} else if (selectedIndex >= gridColumns) { } else if (selectedIndex >= gridColumns) {
// Move up one row
selectedIndex -= gridColumns selectedIndex -= gridColumns
} else if (selectedIndex > 0 && gridColumns === 1) {
selectedIndex--
} else if (currentPath !== homeDir) { } else if (currentPath !== homeDir) {
// At top row, go to back button
backButtonFocused = true backButtonFocused = true
selectedIndex = -1 selectedIndex = -1
} }
@@ -408,13 +523,15 @@ DankModal {
if (backButtonFocused) { if (backButtonFocused) {
backButtonFocused = false backButtonFocused = false
selectedIndex = 0 selectedIndex = 0
} else if (gridColumns === 1) {
if (selectedIndex < totalItems - 1) {
selectedIndex++
}
} else { } else {
// Move down one row if possible
var newIndex = selectedIndex + gridColumns var newIndex = selectedIndex + gridColumns
if (newIndex < totalItems) { if (newIndex < totalItems) {
selectedIndex = newIndex selectedIndex = newIndex
} else { } else {
// If can't go down a full row, go to last item in the column if exists
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
var col = selectedIndex % gridColumns var col = selectedIndex % gridColumns
var targetIndex = lastRowStart + col var targetIndex = lastRowStart + col
@@ -431,7 +548,6 @@ DankModal {
if (backButtonFocused) if (backButtonFocused)
navigateUp() navigateUp()
else if (selectedIndex >= 0 && selectedIndex < totalItems) else if (selectedIndex >= 0 && selectedIndex < totalItems)
// Trigger selection by setting the grid's current index and using signal
fileBrowserModal.keyboardFileSelection(selectedIndex) fileBrowserModal.keyboardFileSelection(selectedIndex)
event.accepted = true event.accepted = true
break break
@@ -446,8 +562,6 @@ DankModal {
interval: 1 interval: 1
onTriggered: { onTriggered: {
// Access the currently selected item through model role names
// This will work because QML models expose role data
executeKeyboardSelection(targetIndex) executeKeyboardSelection(targetIndex)
} }
} }
@@ -485,16 +599,17 @@ DankModal {
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM spacing: 0
spacing: Theme.spacingS
Item { Item {
width: parent.width width: parent.width
height: 40 height: 48
Row { Row {
spacing: Theme.spacingM spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
DankIcon { DankIcon {
name: browserIcon name: browserIcon
@@ -514,9 +629,37 @@ DankModal {
Row { Row {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingS
DankActionButton {
circular: false
iconName: showHiddenFiles ? "visibility_off" : "visibility"
iconSize: Theme.iconSize - 4
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
visible: !weMode
onClicked: showHiddenFiles = !showHiddenFiles
}
DankActionButton {
circular: false
iconName: viewMode === "grid" ? "view_list" : "grid_view"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
visible: !weMode
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
}
DankActionButton {
circular: false
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
visible: !weMode && viewMode === "grid"
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
}
DankActionButton { DankActionButton {
circular: false circular: false
iconName: "movie" iconName: "movie"
@@ -551,56 +694,98 @@ DankModal {
} }
} }
Row { StyledRect {
width: parent.width width: parent.width
spacing: Theme.spacingS 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: fileBrowserModal.quickAccessLocations
currentPath: fileBrowserModal.currentPath
onLocationSelected: path => navigateTo(path)
}
StyledRect { StyledRect {
width: 32 width: 1
height: 32 height: parent.height
radius: Theme.cornerRadius color: Theme.outline
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1 : 0
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: backButtonMouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
} }
} }
StyledText { Column {
text: fileBrowserModal.currentPath.replace("file://", "") width: parent.width - (showSidebar ? 201 : 0)
font.pixelSize: Theme.fontSizeMedium height: parent.height
color: Theme.surfaceText spacing: 0
font.weight: Font.Medium
width: parent.width - 40 - Theme.spacingS FileBrowserNavigation {
elide: Text.ElideMiddle width: parent.width
anchors.verticalCenter: parent.verticalCenter currentPath: fileBrowserModal.currentPath
maximumLineCount: 1 homeDir: fileBrowserModal.homeDir
wrapMode: Text.NoWrap backButtonFocused: fileBrowserModal.backButtonFocused
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
showSidebar: fileBrowserModal.showSidebar
pathEditMode: fileBrowserModal.pathEditMode
onNavigateUp: fileBrowserModal.navigateUp()
onNavigateTo: path => fileBrowserModal.navigateTo(path)
onPathInputFocusChanged: hasFocus => {
fileBrowserModal.pathInputHasFocus = hasFocus
if (hasFocus) {
fileBrowserModal.pathEditMode = true
} }
} }
}
StyledRect {
width: parent.width
height: 1
color: Theme.outline
}
Item {
id: gridContainer
width: parent.width
height: parent.height - 41
clip: true
property real gridCellWidth: weMode ? 255 : iconSizes[iconSizeIndex] + 24
property real gridCellHeight: weMode ? 215 : iconSizes[iconSizeIndex] + 56
property real availableGridWidth: width - Theme.spacingM * 2
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
onGridColumnsChanged: {
fileBrowserModal.actualGridColumns = gridColumns
}
Component.onCompleted: {
fileBrowserModal.actualGridColumns = gridColumns
}
DankGridView { DankGridView {
id: fileGrid id: fileGrid
anchors.fill: parent
width: parent.width anchors.leftMargin: gridContainer.gridLeftMargin
height: parent.height - 80 anchors.rightMargin: Theme.spacingM
clip: true anchors.topMargin: Theme.spacingS
cellWidth: weMode ? 255 : 150 anchors.bottomMargin: Theme.spacingS
cellHeight: weMode ? 215 : 130 visible: viewMode === "grid"
cellWidth: gridContainer.gridCellWidth
cellHeight: gridContainer.gridCellHeight
cacheBuffer: 260 cacheBuffer: 260
model: folderModel model: folderModel
currentIndex: selectedIndex currentIndex: selectedIndex
@@ -609,151 +794,110 @@ DankModal {
positionViewAtIndex(currentIndex, GridView.Contain) positionViewAtIndex(currentIndex, GridView.Contain)
} }
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: DankScrollbar {
policy: ScrollBar.AsNeeded id: gridScrollbar
} }
ScrollBar.horizontal: ScrollBar { ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff policy: ScrollBar.AlwaysOff
} }
delegate: StyledRect { delegate: FileBrowserGridDelegate {
id: delegateRoot weMode: fileBrowserModal.weMode
iconSizes: fileBrowserModal.iconSizes
required property bool fileIsDir iconSizeIndex: fileBrowserModal.iconSizeIndex
required property string filePath selectedIndex: fileBrowserModal.selectedIndex
required property string fileName keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
required property int index onItemClicked: (index, path, name, isDir) => {
selectedIndex = index
width: weMode ? 245 : 140 setSelectedFileData(path, name, isDir)
height: weMode ? 205 : 120 if (weMode && isDir) {
radius: Theme.cornerRadius var sceneId = path.split("/").pop()
color: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
return Theme.surfacePressed
return mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
}
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.outline
border.width: (mouseArea.containsMouse || (keyboardNavigationActive && delegateRoot.index === selectedIndex)) ? 1 : 0
// Update file info when this item gets selected via keyboard or initially
Component.onCompleted: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
// Watch for selectedIndex changes to update file info during keyboard navigation
Connections {
function onSelectedIndexChanged() {
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
}
target: fileBrowserModal
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Item {
width: weMode ? 225 : 80
height: weMode ? 165 : 60
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
anchors.fill: parent
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
property int weExtIndex: 0
source: {
if (weMode && delegateRoot.fileIsDir) {
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
}
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
}
onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
} else {
source = ""
}
}
}
fillMode: Image.PreserveAspectCrop
visible: (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)
maxCacheSize: weMode ? 225 : 80
}
DankIcon {
anchors.centerIn: parent
name: "description"
size: Theme.iconSizeLarge
color: Theme.primary
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
}
DankIcon {
anchors.centerIn: parent
name: "folder"
size: Theme.iconSizeLarge
color: Theme.primary
visible: delegateRoot.fileIsDir && !weMode
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Update selected file info and index first
selectedIndex = delegateRoot.index
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir)
if (weMode && delegateRoot.fileIsDir) {
var sceneId = delegateRoot.filePath.split("/").pop()
fileSelected("we:" + sceneId) fileSelected("we:" + sceneId)
fileBrowserModal.close() fileBrowserModal.close()
} else if (delegateRoot.fileIsDir) { } else if (isDir) {
navigateTo(delegateRoot.filePath) navigateTo(path)
} else { } else {
fileSelected(delegateRoot.filePath) fileSelected(path)
fileBrowserModal.close() fileBrowserModal.close()
} }
} }
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir)
} }
// Handle keyboard selection
Connections { Connections {
function onKeyboardSelectionRequestedChanged() { function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === delegateRoot.index) { if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false fileBrowserModal.keyboardSelectionRequested = false
selectedIndex = delegateRoot.index selectedIndex = index
setSelectedFileData(delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) setSelectedFileData(filePath, fileName, fileIsDir)
if (weMode && delegateRoot.fileIsDir) { if (weMode && fileIsDir) {
var sceneId = delegateRoot.filePath.split("/").pop() var sceneId = filePath.split("/").pop()
fileSelected("we:" + sceneId) fileSelected("we:" + sceneId)
fileBrowserModal.close() fileBrowserModal.close()
} else if (delegateRoot.fileIsDir) { } else if (fileIsDir) {
navigateTo(delegateRoot.filePath) navigateTo(filePath)
} else { } else {
fileSelected(delegateRoot.filePath) fileSelected(filePath)
fileBrowserModal.close()
}
}
}
target: fileBrowserModal
}
}
}
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: fileBrowserModal.selectedIndex
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
onItemClicked: (index, path, name, isDir) => {
selectedIndex = index
setSelectedFileData(path, name, isDir)
if (isDir) {
navigateTo(path)
} else {
fileSelected(path)
fileBrowserModal.close()
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir)
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false
selectedIndex = index
setSelectedFileData(filePath, fileName, fileIsDir)
if (fileIsDir) {
navigateTo(filePath)
} else {
fileSelected(filePath)
fileBrowserModal.close() fileBrowserModal.close()
} }
} }
@@ -764,78 +908,18 @@ DankModal {
} }
} }
} }
}
}
Row { FileBrowserSaveRow {
id: saveRow
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
height: saveMode ? 40 : 0 saveMode: fileBrowserModal.saveMode
visible: saveMode defaultFileName: fileBrowserModal.defaultFileName
spacing: Theme.spacingM currentPath: fileBrowserModal.currentPath
onSaveRequested: filePath => handleSaveFile(filePath)
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() !== "") {
// Remove file:// protocol from currentPath if present for proper construction
var basePath = currentPath.replace(/^file:\/\//, '')
var fullPath = basePath + "/" + text.trim()
// Ensure consistent path format - remove any double slashes and normalize
fullPath = fullPath.replace(/\/+/g, '/')
handleSaveFile(fullPath)
}
}
}
StyledRect {
id: saveButton
width: 80
height: 40
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: I18n.tr("Save")
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
}
StateLayer {
stateColor: Theme.primary
cornerRadius: Theme.cornerRadius
enabled: fileNameInput.text.trim() !== ""
onClicked: {
if (fileNameInput.text.trim() !== "") {
// Remove file:// protocol from currentPath if present for proper construction
var basePath = currentPath.replace(/^file:\/\//, '')
var fullPath = basePath + "/" + fileNameInput.text.trim()
// Ensure consistent path format - remove any double slashes and normalize
fullPath = fullPath.replace(/\/+/g, '/')
handleSaveFile(fullPath)
}
}
}
}
} }
KeyboardHints { KeyboardHints {
@@ -870,134 +954,39 @@ DankModal {
} }
} }
// Overwrite confirmation dialog FileBrowserSortMenu {
Item { id: sortMenu
id: overwriteDialog anchors.top: parent.top
anchors.fill: parent anchors.right: parent.right
visible: showOverwriteConfirmation anchors.topMargin: 120
anchors.rightMargin: Theme.spacingL
Keys.onEscapePressed: { sortBy: fileBrowserModal.sortBy
showOverwriteConfirmation = false sortAscending: fileBrowserModal.sortAscending
pendingFilePath = "" onSortBySelected: value => {
fileBrowserModal.sortBy = value
}
onSortOrderSelected: ascending => {
fileBrowserModal.sortAscending = ascending
}
}
}
} }
Keys.onReturnPressed: { FileBrowserOverwriteDialog {
anchors.fill: parent
showDialog: showOverwriteConfirmation
pendingFilePath: fileBrowserModal.pendingFilePath
onConfirmed: filePath => {
showOverwriteConfirmation = false showOverwriteConfirmation = false
fileSelected(pendingFilePath) fileSelected(filePath)
pendingFilePath = "" pendingFilePath = ""
Qt.callLater(() => fileBrowserModal.close()) Qt.callLater(() => fileBrowserModal.close())
} }
onCancelled: {
focus: showOverwriteConfirmation
Rectangle {
anchors.fill: parent
color: Theme.shadowStrong
opacity: 0.8
MouseArea {
anchors.fill: parent
onClicked: {
showOverwriteConfirmation = false showOverwriteConfirmation = false
pendingFilePath = "" pendingFilePath = ""
} }
} }
} }
StyledRect {
anchors.centerIn: parent
width: 400
height: 160
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingM
StyledText {
text: I18n.tr("File Already Exists")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("A file with this name already exists. Do you want to overwrite it?")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
StyledRect {
width: 80
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
StyledText {
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showOverwriteConfirmation = false
pendingFilePath = ""
}
}
}
StyledRect {
width: 90
height: 36
radius: Theme.cornerRadius
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
anchors.centerIn: parent
text: I18n.tr("Overwrite")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: overwriteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showOverwriteConfirmation = false
fileSelected(pendingFilePath)
pendingFilePath = ""
Qt.callLater(() => fileBrowserModal.close())
}
}
}
}
}
}
}
}
} }
} }

View File

@@ -0,0 +1,130 @@
import QtQuick
import qs.Common
import qs.Widgets
Row {
id: navigation
property string currentPath: ""
property string homeDir: ""
property bool backButtonFocused: false
property bool keyboardNavigationActive: false
property bool showSidebar: true
property bool pathEditMode: false
property bool pathInputHasFocus: false
signal navigateUp()
signal navigateTo(string path)
signal pathInputFocusChanged(bool hasFocus)
height: 40
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1 : 0
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: backButtonMouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigation.navigateUp()
}
}
Item {
width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
height: 32
anchors.verticalCenter: parent.verticalCenter
StyledRect {
anchors.fill: parent
radius: Theme.cornerRadius
color: pathEditMode ? Theme.surfaceContainer : "transparent"
border.color: pathEditMode ? Theme.primary : "transparent"
border.width: pathEditMode ? 1 : 0
visible: !pathEditMode
StyledText {
id: pathDisplay
text: currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
elide: Text.ElideMiddle
verticalAlignment: Text.AlignVCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
pathEditMode = true
pathInput.text = currentPath.replace("file://", "")
Qt.callLater(() => pathInput.forceActiveFocus())
}
}
}
DankTextField {
id: pathInput
anchors.fill: parent
visible: pathEditMode
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onAccepted: {
const newPath = text.trim()
if (newPath !== "") {
navigation.navigateTo(newPath)
}
pathEditMode = false
}
Keys.onEscapePressed: {
pathEditMode = false
}
Keys.onDownPressed: {
pathEditMode = false
}
onActiveFocusChanged: {
navigation.pathInputFocusChanged(activeFocus)
if (!activeFocus && pathEditMode) {
pathEditMode = false
}
}
}
}
Row {
spacing: Theme.spacingXS
visible: !showSidebar
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
circular: false
iconName: "sort"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
}
}
}

View File

@@ -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)
}
}
}
}
}
}
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: sidebar
property var quickAccessLocations: []
property string currentPath: ""
signal locationSelected(string path)
width: 200
color: Theme.surface
clip: true
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 4
StyledText {
text: "Quick Access"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
leftPadding: Theme.spacingS
bottomPadding: Theme.spacingXS
}
Repeater {
model: quickAccessLocations
StyledRect {
width: parent?.width ?? 0
height: 38
radius: Theme.cornerRadius
color: quickAccessMouseArea.containsMouse ? Theme.surfaceContainerHigh : (currentPath === modelData?.path ? Theme.surfacePressed : "transparent")
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: modelData?.icon ?? ""
size: Theme.iconSize - 2
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: quickAccessMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: locationSelected(modelData?.path ?? "")
}
}
}
}
}

View File

@@ -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
}
}
}
}
}