1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-08 06:25:37 -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 profileLastPath: ""
property var fileBrowserSettings: ({
"wallpaper": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"profile": {
"lastPath": "",
"viewMode": "grid",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_save": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"notepad_load": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"generic": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
},
"default": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
})
Component.onCompleted: {
if (!isGreeterMode) {
loadCache()
@@ -43,6 +94,37 @@ Singleton {
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""
if (cache.fileBrowserSettings !== undefined) {
fileBrowserSettings = cache.fileBrowserSettings
} else if (cache.fileBrowserViewMode !== undefined) {
fileBrowserSettings = {
"wallpaper": {
"lastPath": cache.wallpaperLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"profile": {
"lastPath": cache.profileLastPath || "",
"viewMode": cache.fileBrowserViewMode || "grid",
"sortBy": cache.fileBrowserSortBy || "name",
"sortAscending": cache.fileBrowserSortAscending !== undefined ? cache.fileBrowserSortAscending : true,
"iconSizeIndex": cache.fileBrowserIconSizeIndex !== undefined ? cache.fileBrowserIconSizeIndex : 1,
"showSidebar": cache.fileBrowserShowSidebar !== undefined ? cache.fileBrowserShowSidebar : true
},
"file": {
"lastPath": "",
"viewMode": "list",
"sortBy": "name",
"sortAscending": true,
"iconSizeIndex": 1,
"showSidebar": true
}
}
}
if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache)
cleanupUnusedKeys()
@@ -62,6 +144,7 @@ Singleton {
cacheFile.setText(JSON.stringify({
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion
}, null, 2))
}
@@ -74,6 +157,7 @@ Singleton {
const validKeys = [
"wallpaperLastPath",
"profileLastPath",
"fileBrowserSettings",
"configVersion"
]

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

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